To request access to the Fin Agent API, go to Settings > Integrations > Fin Agent API in your workspace and tell us about your use case. Once access is granted, Fin Agent API appears under Fin AI Agent > Deploy.
The Fin Agent API is available on API version 2.14 and above. Make sure your integration uses version 2.14 or later.
Fin can be accessed programmatically via an API. You call Fin from your own agent, and Fin notifies your application of its status and responses through a set of events, delivered via webhooks.
Once Fin is initialized, it works through a series of statuses — thinking when it starts running a procedure and replying as it sends each part of an answer. Fin hands control back to your agent when it reaches awaiting_user_reply (it needs the user's next message to continue) or complete (it has finished). Allow Fin to continue uninterrupted until it returns control with one of these statuses.
You can use Fin in one of two ways, depending on how much control you want to keep over the conversation.
Use Fin as a tool — Your agent stays in control of the turn. It disambiguates the question and decides whether to route to Fin. If it does, it can call Fin capabilities — ask, escalate, or run a procedure — and use the result. Best when you have your own orchestration logic and want Fin to deliver contained answers.

Start a conversation with Fin — Your agent hands the whole turn to Fin. Fin handles the request using its own features: content, guidance, escalations, and procedures. It produces an answer and passes control back. Best when you want Fin to own the resolution end-to-end.

If you are driving Fin from your own agent, the orchestration endpoints let your agent decide what Fin should do and when:
| ENDPOINT | WHEN TO USE |
|---|---|
POST /fin/capabilities | Discover, per user, which procedures and actions are available — call this first to decide what to do. |
POST /fin/ask | Get a single, self-contained answer to one question. Fin will not ask follow-ups, run procedures, or decide to hand over to a human itself. Escalation rules can still match. |
POST /fin/procedures/{procedure_id}/run | Deterministically run a specific procedure. |
POST /fin/reply | Send a follow-up message to an existing conversation — typically when Fin needs more information to complete an action. |
POST /fin/escalate | Hand the conversation off to a human teammate. |
POST /fin/capabilities returns a per-user list of what Fin can do, so your agent can decide which endpoint to call. The list is audience-matched to the user you pass.
| KEY | TYPE | Required | DESCRIPTION |
|---|---|---|---|
user | object | true | The user to list capabilities for. If no user exists for the id, one is created from the supplied details. |
user.id | string | true | The external reference for the user (maps to user_id on the Intercom User object). |
user.name | string | false | The user's name. Applied when the user is created. |
user.email | string | false | The user's email. Applied when the user is created, and updates the existing user's email if it differs. See User Handling. |
user.attributes | object | false | Up to 10 custom attributes. Merged into the user whether it is created or already exists. See User Handling. |
curl -X POST https://api.intercom.io/fin/capabilities \
-H "Authorization: Bearer <ACCESS_TOKEN>" \
-H "Intercom-Version: 2.16" \
-H "Content-Type: application/json" \
-d '{ "user": { "id": "123456" } }'{
"version": "2.16",
"capabilities": [
{
"name": "run_procedure_reset_password_12345",
"description": "<untrusted-content>\nWalk the user through resetting their password.\n</untrusted-content>",
"inputSchema": {
"type": "object",
"properties": { "conversation_id": { "type": "string" }, "user": { "type": "object" }, "message": { "type": "object" }, "conversation_metadata": { "type": "object" } },
"required": ["conversation_id", "user"]
},
"endpoint": "/fin/procedures/12345/run",
"method": "POST"
},
{
"name": "reply",
"description": "Send a user reply into an existing Fin conversation and get Fin's next turn. ...",
"inputSchema": { "type": "object", "properties": { "conversation_id": { "type": "string" }, "message": { "type": "object" }, "user": { "type": "object" } }, "required": ["conversation_id", "message", "user"] },
"endpoint": "/fin/reply",
"method": "POST"
}
]
}Each capability is a tool definition: name, description, inputSchema (a JSON Schema for the request body to send), plus the endpoint and method to call. Procedures are named run_procedure_<title_slug>_<id>, with the slug cut to 80 characters, or run_procedure_<id> when the title gives no usable slug (for example a blank or non-Latin title), and a procedure's description is wrapped in <untrusted-content> tags. The static actions are named reply, ask and escalate. The example above is abbreviated: inputSchema is shortened, and ask and escalate are omitted.
POST /fin/ask gets a single, self-contained answer to one question. It is non-conversational: Fin will not ask follow-up questions, run procedures, or decide to hand over to a human itself. Escalation rules can still match, and the conversation can still be routed to a team or finish without resolution. You can also escalate one yourself with /fin/escalate — the ask conversation is already closed after its one-shot answer, and escalation leaves it closed while routing the handoff separately.
| KEY | TYPE | Required | DESCRIPTION |
|---|---|---|---|
conversation_id | string | true | Your external conversation ID. Fin creates a conversation for this ID. If a conversation already exists for it, use /fin/reply instead. |
message | object | true | The user's question. |
message.author | string | true | Who sent the message. One of user, agent, or fin. |
message.body | string | true | The text of the message. |
message.timestamp | string | false | When the message was sent. Send timestamp or timestamp_ms; at least one is required, and without one the request fails with a 422. If you send both, timestamp_ms is used. It is used to deduplicate messages sent within a 5-minute window. |
user | User | true | The user asking the question. Requires id. |
conversation_metadata | object | false | Optional history (up to 10 prior messages) and attributes to give Fin context. Each history item needs body, an author of user or agent, and a timestamp or timestamp_ms; if any item is invalid, Fin ignores the whole history. |
attachments | array | false | Up to 10 attachments to include with the message. |
environment | string | false | The identifier of the environment to start the conversation in. Only Fin for Platforms workspaces use it; other workspaces ignore it. Use the same value as messenger_environment in your Messenger installation. Defaults to the workspace's default environment. On a Fin for Platforms workspace, an identifier that matches no environment returns a 422. |
response_format | string | false | The format of message.body in Fin's fin_replied events: html (the default) or markdown. If Fin cannot convert a reply to Markdown it sends html instead, so render each reply according to message.format. Requesting markdown on a workspace where it is not enabled returns a 403. The format applies per request, so repeat it on each /fin/reply; a request that omits it gets html. |
curl -X POST https://api.intercom.io/fin/ask \
-H "Authorization: Bearer <ACCESS_TOKEN>" \
-H "Intercom-Version: 2.16" \
-H "Content-Type: application/json" \
-d '{
"conversation_id": "ext-123",
"message": { "author": "user", "body": "How do I reset my password?", "timestamp": "2025-01-24T10:01:20.000Z" },
"user": { "id": "123456", "name": "John Doe", "email": "john.doe@example.com" }
}'| KEY | TYPE | DESCRIPTION |
|---|---|---|
conversation_id | String | The external ID of the conversation. |
user_id | String | The ID of the user. |
status | Enum<String> | Fin's status when the request was accepted, always thinking. Later statuses arrive as events. |
created_at_ms | String | The timestamp the response was created at, with millisecond precision. |
errors | object | Optional. Per-field errors for non-fatal issues, such as user attributes that could not be applied. |
{
"conversation_id": "ext-123",
"user_id": "user-456",
"status": "thinking",
"created_at_ms": "2025-01-24T10:00:00.123Z"
}Fin's answer arrives asynchronously via the fin_replied event. The conversation ends with a complete status — there is no awaiting_user_reply cycle.
Endpoint: /fin/reply
Use /fin/reply to send a follow-up message in an existing conversation. When Fin needs more information to complete an action, it sets the conversation to awaiting_user_reply — send the user's response so Fin can continue.
This endpoint requires user.id, the same as the other orchestration endpoints.
| KEY | TYPE | Required | DESCRIPTION |
|---|---|---|---|
message | Message | true | The user's message. Requires author, body, and a timestamp or timestamp_ms (if you send both, timestamp_ms is used). Without a timestamp the request fails with a 422. |
conversation_id | String | true | The ID of the conversation. |
user | Object<User> | true | A user object that identifies the user replying to Fin. |
attachments | Array<Attachment> | false | An array of attachments to include with the message. Note: Maximum of 10 attachments. |
response_format | String | false | The format of message.body in Fin's fin_replied events: html (the default) or markdown. If Fin cannot convert a reply to Markdown it sends html instead, so render each reply according to message.format. Requesting markdown on a workspace where it is not enabled returns a 403. The format applies per request, so repeat it on each /fin/reply; a request that omits it gets html. |
Request Payload Example
{
"message": {
"author": "user",
"body": "Here's the information you requested.",
"timestamp": "2025-01-24T09:01:00.000Z"
},
"conversation_id": "ext-123",
"user": {
"id": "123456",
"name": "John Doe",
"email": "john.doe@example.com",
"attributes": {
"plan_type": "Pro",
"subscription_status": "active"
}
},
"attachments": [
{
"type": "url",
"url": "https://example.com/invoice.pdf"
}
]
}| KEY | TYPE | DESCRIPTION |
|---|---|---|
conversation_id | String | The ID of the conversation. |
user_id | String | The ID of the user. |
status | Enum<String> | Fin's status. |
created_at_ms | String | The timestamp the response was created at, with millisecond precision. Example: 2025-01-24T10:00:00.123Z |
errors | Object | Optional. Contains error details if any user attribute updates failed. |
errors.user.attributes | Hash<String,String> | Optional. Map of user attribute names to error messages. |
Response Example
{
"conversation_id": "ext-123",
"user_id": "user-456",
"status": "thinking",
"created_at_ms": "2025-01-24T10:00:00.123Z",
"errors": {
"user": {
"attributes": {
"invalid_attr": "User attribute 'invalid_attr' does not exist"
}
}
}
}POST /fin/procedures/{procedure_id}/run deterministically runs a specific procedure on a new conversation. Calling it guarantees that procedure runs, or fails with a 422, for example when the procedure doesn't exist, isn't live, isn't available on the Fin Agent API channel or doesn't match the user's audience, when no Fin profile is configured to handle the conversation, or when conversation_id is already in use.
| KEY | TYPE | Required | DESCRIPTION |
|---|---|---|---|
procedure_id (path) | string | true | The ID of the procedure to run. |
conversation_id | string | true | Your external conversation ID. Fin creates a conversation for this ID. If a conversation already exists for it, use /fin/reply instead. |
user | User | true | The user the procedure runs for. Requires id. |
message | object | false | An optional trigger message. If provided, body is required. author (user, agent or fin) is accepted but has no effect: the message is always posted as an operator message. |
conversation_metadata | object | false | Optional conversation attributes (no history). |
environment | string | false | The identifier of the environment to start the conversation in. Only Fin for Platforms workspaces use it; other workspaces ignore it. Use the same value as messenger_environment in your Messenger installation. Defaults to the workspace's default environment. On a Fin for Platforms workspace, an identifier that matches no environment returns a 422. |
curl -X POST https://api.intercom.io/fin/procedures/12345/run \
-H "Authorization: Bearer <ACCESS_TOKEN>" \
-H "Intercom-Version: 2.16" \
-H "Content-Type: application/json" \
-d '{
"conversation_id": "ext-123",
"user": { "id": "123456", "name": "John Doe", "email": "john.doe@example.com" }
}'| KEY | TYPE | DESCRIPTION |
|---|---|---|
conversation_id | String | The external ID of the conversation. |
user_id | String | The ID of the user. |
procedure_id | String | The ID of the procedure that was run. |
status | Enum<String> | Fin's status when the request was accepted, always thinking. Later statuses arrive as events. |
created_at_ms | String | The timestamp the response was created at, with millisecond precision. |
errors | object | Optional. Per-field errors for non-fatal issues, such as user attributes that could not be applied. |
{
"conversation_id": "ext-123",
"user_id": "user-456",
"procedure_id": "12345",
"status": "thinking",
"created_at_ms": "2025-01-24T10:00:00.123Z"
}If the procedure pauses for input, the conversation status becomes awaiting_user_reply — send the user's response with Reply to Fin.
POST /fin/escalate hands a conversation off to a human teammate. If you use the Intercom Helpdesk, the handoff lands in your team inbox. Provide either conversation_id or user:
conversation_id— escalate an existing agent conversation, including one started with/fin/ask. On the Intercom Helpdesk, Fin by default summarises the conversation and opens a new Helpdesk conversation that carries the summary as an internal note. You can change this default with an escalation Operator Workflow (see below). Escalation closes the original agent conversation if it is open, including on Fin for Platforms.user— escalate on behalf of a user with no prior agent conversation. Fin creates an agent conversation for the escalation and closes it once the handoff completes. On the Intercom Helpdesk, the teammate works in a separate new Helpdesk conversation. Not supported on Fin for Platforms — see below.
In both cases, pass the optional context to give the receiving teammate background your orchestrating agent has and Fin does not. On the Intercom Helpdesk, it appears above the summary in the internal note of the new conversation the teammate picks up. It is never shown to the end user.
On Fin for Platforms, context is not surfaced: escalation hands off to your own agent desk rather than creating an Intercom Helpdesk conversation, and the escalation events your platform receives do not carry it.
Escalating an existing conversation also sets its AI Agent resolution state to escalated, readable as ai_agent.resolution_state on the Conversations API. This is a resolution state, not a billable resolution.
By default, an escalation that no workflow handles opens a new Helpdesk conversation on the Intercom Helpdesk. To change what happens on escalate, configure an Operator Workflow triggered by Fin Agent API escalations. When a workflow matches, it takes over the handoff instead of the default, so no new conversation is opened unless the workflow opens one. The original conversation is closed again once the workflow's first steps have run, so a reassignment or reply from those steps leaves it closed.
There is no Intercom Helpdesk on Fin for Platforms, so there is no Helpdesk conversation to create — user is not supported and is rejected. Escalate an existing Fin conversation with conversation_id instead. No Helpdesk conversation is created for a teammate: you're notified with the escalated status and hand the conversation to a human on your own platform.
| KEY | TYPE | Required | DESCRIPTION |
|---|---|---|---|
conversation_id | string | false | The conversation to escalate. Provide this or user. Required on Fin for Platforms, where user is not supported. |
user | User | false | The user to escalate on behalf of, creating a new conversation. Requires id. Provide this or conversation_id. Not supported on Fin for Platforms. |
context | string | false | Background for the receiving teammate, valid with either conversation_id or user. On the Intercom Helpdesk, it appears above the summary in the internal note of the new conversation the teammate picks up, and is never shown to the end user. Up to 10,000 characters. Not surfaced on Fin for Platforms. Avoid including credentials or unnecessary personal data — it is visible to any teammate with access to the conversation. |
Unlike the other orchestration endpoints, user is optional here. Escalate accepts either conversation_id (to escalate an existing agent conversation) or user (to escalate on behalf of a user with no prior conversation) — exactly one of the two is required. On Fin for Platforms only conversation_id is accepted.
| KEY | TYPE | DESCRIPTION |
|---|---|---|
conversation_id | String | Your external conversation ID. When you escalate an existing conversation by conversation_id, this echoes the ID you sent. When you escalate on behalf of a user, Fin creates an agent conversation for the escalation and generates this ID for it (in the form escalate-<hex>). |
status | Enum<String> | The resulting status of the conversation. Value: escalated |
intercom_conversation_id | String | Optional. The internal Intercom ID of the agent conversation created for the escalation. Returned only when escalating on behalf of a user. Fin closes this conversation once the handoff completes; the teammate works in a separate Helpdesk conversation. |
You are notified over webhooks with an escalated status followed by complete. complete signals that Fin is done. By then the original agent conversation has been closed. An escalation Operator Workflow may act on it afterwards, but it is closed again once the workflow's first steps have run, so only steps that run later (for example after a wait) can reopen it. On the Intercom Helpdesk the handoff continues in the new conversation routed to your team inbox, which stays open there, and on Fin for Platforms you hand the conversation to a human on your own platform.
curl -X POST https://api.intercom.io/fin/escalate \
-H "Authorization: Bearer <ACCESS_TOKEN>" \
-H "Intercom-Version: 2.16" \
-H "Content-Type: application/json" \
-d '{ "conversation_id": "ext-123", "context": "Customer is requesting a refund." }'{
"conversation_id": "ext-123",
"status": "escalated"
}When escalating on behalf of a user, Fin creates an agent conversation for the escalation and closes it once the handoff completes. The response returns the conversation_id Fin generated for it, along with its intercom_conversation_id:
{
"conversation_id": "escalate-1a2b3c4d5e6f7a8b",
"intercom_conversation_id": "987654321",
"status": "escalated"
}Pricing for the Fin Agent API is being finalised and is subject to change.
Conversations you drive through the Fin Agent API are billed the same way as any other Fin conversation — on outcomes. You're charged at most once per conversation, and only when Fin reaches a billable outcome.
A conversation that Fin answers through the API is counted as a resolution, which is a billable outcome. Conversations that don't reach a billable outcome aren't charged — for example, handing a conversation to a human with /fin/escalate without a resolution, or discovering what Fin can do with /fin/capabilities, which doesn't create a conversation.
For how outcomes are calculated and billed, see Understanding Fin outcomes.
Endpoint: /fin/start
Initialise Fin by passing it the user's message along with some conversation history and user details. These additional pieces of context will be used by Fin to provide a better and more contextual answer to the user.
| KEY | TYPE | Required | DESCRIPTION |
|---|---|---|---|
message | Message | true | The message that the user is sending to Fin. Requires author, body, and a timestamp or timestamp_ms (if you send both, timestamp_ms is used). Without a timestamp the request fails with a 422. |
conversation_id | String | true | The ID of the conversation that is calling Fin via this API. |
user | Object<User> | true | A user object that identifies the user making the query. |
attachments | Array<Attachment> | false | An array of attachments to include with the message. Note: Maximum of 10 attachments. |
conversation_metadata | Object | false | Metadata about the conversation, including history and attributes. |
conversation_metadata.history | Array<Message> | false | An array of previous messages of the conversation before Fin is initialised. This data provides context to Fin on the history of the conversation and helps generate a better answer. Note: Limit to the last 10 messages. Each item needs body, an author of user or agent, and a timestamp or timestamp_ms; if any item is invalid, Fin ignores the whole history. |
conversation_metadata.attributes | Hash<String,Object> | false | A Hash of attributes associated with the conversation. These attributes can be used by Fin to provide more contextual responses. Note: Limit to 20 attributes. |
environment | String | false | The identifier of the environment to start the conversation in. Only Fin for Platforms workspaces use it; other workspaces ignore it. Use the same value as messenger_environment in your Messenger installation. Defaults to the workspace's default environment. On a Fin for Platforms workspace, an identifier that matches no environment returns a 422. |
response_format | String | false | The format of message.body in Fin's fin_replied events: html (the default) or markdown. If Fin cannot convert a reply to Markdown it sends html instead, so render each reply according to message.format. Requesting markdown on a workspace where it is not enabled returns a 403. The format applies per request, so repeat it on each /fin/reply; a request that omits it gets html. |
Request Payload Example
{
"conversation_id": "ext-123",
"message": {
"author": "user",
"body": "How can I see my account details?",
"timestamp": "2025-01-24T10:01:20.000Z"
},
"user": {
"id": "123456",
"name": "John Doe",
"email": "john.doe@example.com",
"attributes": {
"plan_type": "Pro",
"subscription_status": "active"
}
},
"attachments": [
{
"type": "url",
"url": "https://example.com/document.pdf"
},
{
"type": "file",
"name": "screenshot.png",
"content_type": "image/png",
"data": "<base64 encoded file data>"
}
],
"conversation_metadata": {
"history": [
{
"author": "user",
"body": "I need help",
"timestamp": "2025-01-24T10:00:01Z"
},
{
"author": "agent",
"body": "What do you need help with?",
"timestamp": "2025-01-24T10:01:00Z"
}
],
"attributes": {
"priority_level": "high",
"department": "sales"
}
}
}| KEY | TYPE | DESCRIPTION |
|---|---|---|
conversation_id | String | The ID of the conversation. |
user_id | String | The ID of the user. |
status | Enum<String> | Fin's status. |
created_at_ms | String | The timestamp the response was created at, with millisecond precision. Example: 2025-01-24T10:00:00.123Z |
errors | Object | Optional. Contains error details if any user or conversation attribute updates failed. |
errors.user.attributes | Hash<String,String> | Optional. Map of user attribute names to error messages. |
errors.conversation.attributes | Hash<String,String> | Optional. Map of conversation attribute names to error messages. |
Response Example
{
"conversation_id": "ext-123",
"user_id": "user-456",
"status": "thinking",
"created_at_ms": "2025-01-24T10:00:00.123Z",
"errors": {
"user": {
"attributes": {
"invalid_attr": "User attribute 'invalid_attr' does not exist"
}
},
"conversation": {
"attributes": {
"bad_attr": "Conversation attribute 'bad_attr' does not exist",
"priority_level": "'1234' is not a valid value for attribute 'priority_level' of type 'string'"
}
}
}
}Conversations you drive through /fin/start are billed on outcomes. You're charged at most once per conversation, and only when Fin reaches a billable outcome such as a resolution.
For how outcomes are calculated and billed, see Understanding Fin outcomes.
POST /fin/csat records a customer's satisfaction rating for a conversation, with an optional free-text remark.
Fin asks for a rating when a conversation reaches a point where CSAT collection is enabled — this reuses the CSAT settings on your Fin workflow, so you control when a rating is requested from Intercom, not through this API. When Fin asks, it fires a csat_requested event over webhooks carrying the rating options to show the user. Present those options, then submit the user's choice with this endpoint.
POST /fin/csat and the csat_requested event are available on API version 2.14 and above.
| KEY | TYPE | Required | DESCRIPTION |
|---|---|---|---|
conversation_id | String | true | Your external conversation ID — the same ID you started the conversation with and the one echoed on the csat_requested event. |
rating | Enum<String> | true | The rating the user selected — one of the key values from the csat_requested event's options. Values: terrible bad ok good amazing |
remark | String | false | An optional free-text comment the user left alongside the rating. |
| KEY | TYPE | DESCRIPTION |
|---|---|---|
conversation_id | String | The external ID of the rated conversation. |
rating | Enum<String> | The rating now recorded on the conversation. Values: terrible bad ok good amazing |
status | Enum<String> | The result of the submission. Value: rated |
Submitting the same rating again (with no new remark) is a no-op that succeeds even after the rating window has closed, so an at-least-once client can safely retry. Submitting a different rating updates the stored rating, but only while the rating window is still open. A first remark can be added to an already-rated survey — even after the window has closed — but once a remark has been recorded, the rating is permanently locked and can no longer be changed.
curl -X POST https://api.intercom.io/fin/csat \
-H "Authorization: Bearer <ACCESS_TOKEN>" \
-H "Intercom-Version: 2.16" \
-H "Content-Type: application/json" \
-d '{
"conversation_id": "ext-123",
"rating": "amazing",
"remark": "Fin solved my problem in seconds."
}'{
"conversation_id": "ext-123",
"rating": "amazing",
"status": "rated"
}A rating that can't be recorded returns a 422 with an errors message. Common causes:
| CAUSE | DESCRIPTION |
|---|---|
| Unknown conversation | No conversation exists for the given conversation_id. |
| No rating requested | The conversation has no rating to record — Fin never fired a csat_requested event for it. |
| Rating window closed | The time window for rating this conversation has passed, per your workspace's CSAT settings. |
| Invalid rating | rating is not one of the supported values. |
| Rating locked | A remark has already been submitted for this rating, so it can no longer be changed. |
Fin fires events during its workflow to convey its status and deliver replies to the user. These events are delivered via Webhooks.
A webhook endpoint can be configured in the Fin Agent API settings. The client can then listen for Fin events sent to this webhook and handle them accordingly.
All webhook requests will include an X-Fin-Agent-API-Webhook-Signature header containing an HMAC-SHA256 signature of the request body. Validation can be done by generating a signature using the request body and the signing secret from the settings, and comparing it with the aforementioned header value.
Fin will report its status to the client via this event.
| KEY | TYPE | DESCRIPTION |
|---|---|---|
event_name | String | The name of the event. Value: fin_status_updated |
conversation_id | String | The ID of the conversation. |
user_id | String | The ID of the user. |
status | Enum<String> | Fin's current status. Supported values: thinking escalated resolved complete awaiting_user_reply |
reason | String | Optional. A human-readable explanation of why the conversation was escalated. Only present when status is escalated. See Escalation Reasons for possible values. |
created_at_ms | String | The timestamp the event was created at, with millisecond precision. Example: 2025-01-24T10:00:00.123Z |
Status Update Example (Escalated)
{
"event_name": "fin_status_updated",
"conversation_id": "ext-123",
"user_id": "7891",
"status": "escalated",
"reason": "Escalation requested by user",
"created_at_ms": "2025-01-24T10:00:00.123Z"
}Status Update Example (Resolved)
{
"event_name": "fin_status_updated",
"conversation_id": "ext-123",
"user_id": "7891",
"status": "resolved",
"created_at_ms": "2025-01-24T10:00:00.123Z"
}Status Update Example (Awaiting User Reply)
{
"event_name": "fin_status_updated",
"conversation_id": "ext-123",
"user_id": "7891",
"status": "awaiting_user_reply",
"created_at_ms": "2025-01-24T10:00:00.123Z"
}The fin_status_updated event with status awaiting_user_reply fires once Fin's reply is done — all reply parts have been sent. Use this event to know when the user can reply.
Fin will reply to a user via this event. The content of the response will be contained in the Message object. Intermediate reply events have a status of replying, indicating that more reply parts may follow. Once Fin's reply is done, a separate fin_status_updated event with status awaiting_user_reply is fired.
The replying status for intermediate fin_replied events — and the fin_status_updated event with awaiting_user_reply that fires once Fin's reply is done — require API version 2.16 and above. On version 2.15 and earlier, every fin_replied event uses awaiting_user_reply as the status and no fin_status_updated event with that status is sent. See the 2.16 changelog for details.
Once Fin finishes and the conversation reaches complete, a csat_requested survey may follow if CSAT is enabled on your Fin workflow.
| KEY | TYPE | DESCRIPTION |
|---|---|---|
event_name | String | The name of the event. Value: fin_replied |
conversation_id | String | The ID of the conversation. |
user_id | String | The ID of the user. |
message | Message | Fin's answer to the user's query. Includes timestamp_ms for millisecond precision timing. |
status | Enum<String> | Fin's current status. Values: replying (intermediate reply) awaiting_user_reply (legacy; see the note above) complete (Fin's reply is final and no user reply is expected; sent for one-shot Ask Fin replies only) |
message.id | String | A unique identifier for this message. |
message.sources | Array<Source> | Optional. Available on API version 2.14 and above. The content sources Fin used to generate this reply, in the same order as the [n] citation markers in message.body. Present only when Fin cited content. Each Source has title (String), type (Enum<String>: article or external), and url (String). |
message.format | String | Optional. Only present when the request set response_format to markdown. Value: markdown, or html if Fin could not convert the reply to Markdown and sent HTML instead. When absent, message.body is HTML. |
created_at_ms | String | The timestamp the event was created at, with millisecond precision. Example: 2025-01-24T10:00:00.123Z |
Fin Reply Example (Intermediate)
{
"event_name": "fin_replied",
"conversation_id": "ext-123",
"user_id": "7891",
"message": {
"id": "98765",
"author": "fin",
"body": "<p>You can see your account details by clicking on the <em>Account</em> tab in the top right corner of the screen. [1]</p>",
"timestamp_ms": "2025-01-24T09:01:00.456Z",
"sources": [
{
"title": "Managing your account",
"type": "article",
"url": "https://www.example.com/help/en/articles/1-managing-your-account"
}
]
},
"status": "replying",
"created_at_ms": "2025-01-24T10:00:00.123Z"
}After receiving one or more fin_replied events with status: replying, wait for a fin_status_updated event with status: awaiting_user_reply before prompting the user to reply. This ensures all of Fin's reply parts have been delivered.
Fin fires this event when it asks the user to rate the conversation. It carries the rating options to present; collect the user's choice and submit it with POST /fin/csat. Unlike the reply events it has no message — a rating survey is a set of options to choose from, not readable text.
| KEY | TYPE | DESCRIPTION |
|---|---|---|
event_name | String | The name of the event. Value: csat_requested |
conversation_id | String | The ID of the conversation. |
user_id | String | The ID of the user. |
csat | Object | The rating survey to present to the user. |
csat.options | Array<Object> | The ordered rating options to show the user. |
csat.options[].key | Enum<String> | The stable key to send back as rating on POST /fin/csat. Values: terrible bad ok good amazing |
csat.options[].emoji | String | The emoji representing this rating. |
csat.options[].label | String | The human-readable label for this rating, localized to the conversation's detected language. Distinct from key — display the label, but send back the key. |
created_at_ms | String | The timestamp the event was created at, with millisecond precision. Example: 2025-01-24T10:00:00.123Z |
CSAT Requested Example
{
"event_name": "csat_requested",
"conversation_id": "ext-123",
"user_id": "7891",
"csat": {
"options": [
{ "key": "terrible", "emoji": "😠", "label": "Terrible" },
{ "key": "bad", "emoji": "🙁", "label": "Bad" },
{ "key": "ok", "emoji": "😐", "label": "Okay" },
{ "key": "good", "emoji": "😃", "label": "Great" },
{ "key": "amazing", "emoji": "🤩", "label": "Amazing" }
]
},
"created_at_ms": "2025-01-24T10:00:00.123Z"
}As Fin works through a request it reports a series of statuses. Listen for these via Webhooks and react accordingly.
Which statuses you see depends on the endpoint. thinking is sent when Fin starts running a procedure, not on every turn. A turn ends with awaiting_user_reply when Fin has answered and is waiting for the user's next message (on API version 2.16 and above this is sent as a fin_status_updated event; on 2.15 and earlier it is the status of the fin_replied events), or with complete when Fin is done: it resolved the conversation, escalated, finished a procedure, closed the conversation after the user went inactive, or detected spam. A one-shot Ask Fin call always ends with complete and never enters awaiting_user_reply. replying accompanies each reply part. escalated is covered below.

| STATUS | DESCRIPTION |
|---|---|
thinking | Fin has started running a procedure. This status is not sent on every turn. When Fin is thinking, the client should consider indicating to the user that Fin is working on an answer. |
replying | Fin is sending reply parts. Each fin_replied event with this status contains a part of Fin's answer. Wait for the fin_status_updated event with awaiting_user_reply before prompting the user to reply. |
awaiting_user_reply | Fin has finished replying and is waiting for the user to respond. This status is delivered via the fin_status_updated event once Fin's reply is done, after all fin_replied events. |
escalated | The conversation has been handed off to a human. This is deterministic when you call /fin/escalate. It can also happen automatically when Fin owns the conversation — /fin/start, /fin/procedures/{procedure_id}/run, and /fin/reply on a conversation started that way, let Fin decide to escalate. Fin will not decide to escalate a one-shot /fin/ask conversation, but an escalation rule can, and you can escalate one yourself. |
resolved | The user's query has been resolved. The user has indicated to Fin that it has successfully answered their query. |
complete | Fin has completed its workflow and the conversation is over. Control of the conversation is now back with the client. When CSAT is enabled, a csat_requested survey may follow. |
When a conversation is escalated, the escalated fin_status_updated event carries a reason field.
When you escalate deterministically with /fin/escalate, the reason is always:
| REASON | DESCRIPTION |
|---|---|
Escalation requested via API | You initiated the handoff by calling /fin/escalate. |
The reasons below apply to Fin-decided escalations when Fin owns the conversation — /fin/start, /fin/procedures/{procedure_id}/run, and /fin/reply on a conversation started that way. /fin/ask can report any of them, because escalation rules still apply and the conversation can still be routed to a team or finish without resolution. On an Ask, Escalation requested by user normally only follows an escalation rule match.
The following reasons may be provided:
| REASON | DESCRIPTION |
|---|---|
Escalation requested by user | Fin decided to hand the conversation over, for example because the user asked for a human, a procedure handed off, or a custom answer handed over. It also follows an escalation rule match, as a second escalated event after the rule's reason. While a procedure is running, every escalation Fin decides on reports this reason. |
Escalation rule: {rule_name} | An escalation rule was triggered based on conversation attributes or conditions. The {rule_name} will be replaced with the actual name of the matched rule. |
Escalation rule matched | A generic escalation rule was triggered, but no specific rule name is available. |
Routed to team | The conversation was routed to a specific team based on your routing configuration. |
Conversation finished without resolution | The conversation ended without being resolved or explicitly escalated through one of the above paths. |
Note: The reason field is optional and may not be present in all escalated status update events. Legacy implementations or certain edge cases may not include this field.
All requests to this API require an API key in the HTTP header. You can obtain an API key after completing the Fin Agent API setup.
For general authentication guidelines, see Intercom authentication.
Your API key grants access to the Fin Agent API on behalf of your workspace. Do not share it publicly or expose it in client-side code.
When you provide a User object to the Fin Agent API, the following behavior applies:
User role only: The Fin Agent API only supports the User role. Leads and Visitors are not supported.
User lookup: The
idfield is used to look up existing users in Intercom. This value maps to theuser_idfield on the Intercom User object.User creation: If no user is found with the provided
id, a new user will be created with the given parameters.Email updates: If an
emailis provided and differs from the existing user's email, the user's email will be updated.Attribute updates: If
attributesare provided, they will be merged with the user's existing attributes. New attributes are added and existing attributes with the same key are overwritten.User data update considerationsIf your users interact with Intercom across multiple channels (e.g., Messenger and the Fin Agent API), updating the email or attributes via this API will affect the user record across all channels. Ensure that the data you provide is authoritative and consistent with your other integrations to avoid unintended changes to user data.
fin_replied— Each event carries a part of Fin's answer as rich text (HTML) inmessage.body.fin_status_updated— A follow-upfin_status_updatedevent is fired after Fin's replies to indicate the conversation has moved toawaiting_user_reply,resolved, orcompletestatus. Use this to know once Fin's reply is done.
Use a stable API version (e.g., 2.14, 2.15) for production integrations. The Preview version may include experimental changes that are modified or removed before reaching a stable release. See Update your API version for guidance on choosing and upgrading versions.
By default, the message.body field in fin_replied events contains server-rendered HTML. If you set response_format to markdown, it contains Markdown instead and the HTML elements below do not apply, unless Fin could not convert the reply and sent HTML instead. Render each reply according to message.format. The specific elements that appear depend on the knowledge ingested into Fin — for example, if your help center articles contain tables or code blocks, Fin may include those elements in its responses.
The following HTML elements may appear in message.body:
| Tag | What it does | Details |
|---|---|---|
<p> | Paragraph | Primary text container. |
<h1> <h2> <h3> <h4> | Headings | |
<b> <strong> <i> <em> | Text formatting | Bold and italic. |
<a> | Link | Attributes: href, target. Also used for inline citations (see below). |
<ol> <ul> <li> | Lists | Ordered and unordered. Can be nested. |
<pre> <code> | Code | <pre> wraps <code> for code blocks. Inline <code> for inline snippets. |
<img> | Image | Attributes: src, alt, width, height. Wrapped in a <div> container. |
<table> <tbody> <tr> <td> <th> | Table | Wrapped in a <div>. Cells may have colspan, rowspan. |
<div> | Container | Wraps images, tables, and callout blocks. |
<br> | Line break | |
<hr> | Horizontal rule |
Over time we may add support for new HTML elements in message.body. This will not constitute a breaking change. We advise client code to gracefully handle unknown HTML elements when rendering Fin's responses.