> For the complete documentation index, see [llms.txt](https://en.help.firstline.cc/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://en.help.firstline.cc/developer/api/advanced/headless-conversation-api.md).

# Headless Conversation API

Use the Headless Conversation API to send customer messages from your company’s own systems, receive replies, and handle conversation events.

{% hint style="info" %}
This API is used to integrate "custom pipelines" and is currently in **Beta** feature. Before正式 use, please first complete pipeline creation, receiver settings, and activation in FIRST LINE. Features and interface may be adjusted depending on the version.
{% endhint %}

**Headless Conversation API** Allows enterprises to connect their own website, app, or customer service interface to FIRST LINE. The enterprise is responsible for the front-end interface and user authentication. FIRST LINE processes messages according to pipeline settings and returns replies from AI agents, conversation scripts, or human customer service through events.

If you have not yet created a pipeline, please first read [Custom Pipeline Setup Guide](/feature/channels/headless-api.md).

## Quick Start

1. Create a custom pipeline in FIRST LINE and carefully keep the **Client ID**、**Client Secret** and **Webhook Secret**.
2. Select a receiver (Fancy AI agent or conversation script), complete the required settings, and activate the pipeline.
3. From the enterprise backend, use OAuth 2.0 **Client Credentials** to obtain an Access Token.
4. Call the Send Message API.
5. Use **Webhook** or **Event Polling** to get the reply, then display it in your own interface.

{% hint style="warning" %}
The Client Secret and Webhook Secret are shown only when created or regenerated. Please store them in a trusted backend environment, and do not write them into website front-end code, app source code, or public repositories.
{% endhint %}

## 1. Authentication

The Headless Conversation API uses OAuth 2.0 Client Credentials authorization. The required scope is `headless:use`. Please use the API domain provided by the FIRST LINE tenant and apply for an Access Token with the Client ID and Client Secret obtained when creating the pipeline.

Please send **POST JSON Body** Client Credentials. Do not place `client_id`、`client_secret`、`scope` or other identification/credential data in the URL or query string; some environments may not correctly read parameters in the URL when handling POST requests, and sensitive data may also be retained in server, proxy, or browser URL history.

Request example:

```http
POST https://YOUR_TENANT_HOST/oauth/token
Accept: application/json
Content-Type: application/json

{
  "grant_type": "client_credentials",
  "client_id": "YOUR_CLIENT_ID",
  "client_secret": "YOUR_CLIENT_SECRET",
  "scope": "headless:use"
}
```

cURL example:

```bash
curl -X POST "https://YOUR_TENANT_HOST/oauth/token" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "client_credentials",
    "client_id": "YOUR_CLIENT_ID",
    "client_secret": "YOUR_CLIENT_SECRET",
    "scope": "headless:use"
  }'
```

{% hint style="warning" %}
Do not send credentials in the following way:

```
POST /oauth/token?client_id=...&client_secret=...&scope=...
```

Client ID, Client Secret, and Scope should be sent in the **JSON Request Body** ; do not append them after the URL.
{% endhint %}

{% hint style="info" %}
For production integration, please confirm the tenant's Token Endpoint and credential issuance method; the `YOUR_TENANT_HOST` on this page is only a sample domain.
{% endhint %}

After authorization is complete, include the following in API requests:

```http
Authorization: Bearer YOUR_ACCESS_TOKEN
Accept: application/json
Content-Type: application/json
```

**Note:** The Client Credentials for the Headless Conversation API are different from a normal FIRST LINE employee API Token. Do not mix them up.

## 2. Send Customer Messages

<mark style="color:green;">POST</mark> `/api/headless/v1/messages`

### Send text

```bash
curl -X POST "https://YOUR_TENANT_HOST/api/headless/v1/messages" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "external_user_id": "customer_123",
    "client_message_id": "msg_0001",
    "message": {
      "type": "text",
      "text": "How do I handle the return process?"
    },
    "profile": {
      "name": "Test Customer"
    }
  }'
```

### Request Body

| Field               | Required          | Description                                                                                                                                                             |
| ------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `external_user_id`  | Yes               | The customer identifier value in the enterprise system, up to 191 characters. For the same customer within the same pipeline, continue using the same identifier value. |
| `client_message_id` | No (recommended)  | A unique identifier for this message, up to 191 characters, used for safe resend.                                                                                       |
| `message.type`      | Yes               | `text` or `postback`.                                                                                                                                                   |
| `message.text`      | For text messages | message content, up to 1,000 characters.                                                                                                                                |
| `message.value`     | For Postback      | The value returned after the customer clicks an option, up to 1,000 characters.                                                                                         |
| `profile.name`      | No                | Customer display name, up to 50 characters.                                                                                                                             |
| `context`           | No                | Additional context data; see the explanation below.                                                                                                                     |

Postback example:

```json
{
  "external_user_id": "customer_123",
  "client_message_id": "msg_0002",
  "message": {
    "type": "postback",
    "value": "check_order"
  }
}
```

### Successful response

HTTP `202 Accepted`：

```json
{
  "request_id": "9bfb0e4f-8b67-4f6d-9d34-44e8b3ac6ae2",
  "conversation_id": "CONVERSATION_ID",
  "sequence": 1
}
```

| Field             | Description                                                               |
| ----------------- | ------------------------------------------------------------------------- |
| `request_id`      | The request identifier for this message, used to match subsequent events. |
| `conversation_id` | Conversation identifier; used when querying events.                       |
| `sequence`        | Event sequence number, helpful for handling event order.                  |

{% hint style="info" %}
**HTTP 202 does not mean the AI has finished answering.** After FIRST LINE accepts the message, obtain subsequent messages and processing results via Webhook or event polling.
{% endhint %}

### Avoid duplicate submissions

It is recommended to provide a unique `client_message_id`. When the network times out and you are unsure whether the message was received, retry with**the same identifier and the same content**.

If the original message is still being processed, you may receive `409 Conflict`; if the same identifier is used with different message content, you will receive `422 Unprocessable Entity`. Do not generate a new message ID just for retrying, otherwise duplicate sends may occur.

## 3. Query Conversation Events

<mark style="color:blue;">GET</mark> `/api/headless/v1/conversations/{conversation_id}/events`

You can use the event query API to get new replies and status notifications for a specified conversation:

```bash
curl "https://YOUR_TENANT_HOST/api/headless/v1/conversations/CONVERSATION_ID/events?after_sequence=0" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Accept: application/json"
```

| Parameters        | Description                                                                                               |
| ----------------- | --------------------------------------------------------------------------------------------------------- |
| `conversation_id` | The conversation identifier returned by the Send Message API.                                             |
| `after_sequence`  | Optional; only events with a sequence number greater than the specified value are returned. Default is 0. |

In the response, `data` is the event list and may be an empty array:

```json
{
  "data": [
    {
      "event_id": "evt_1201",
      "type": "message.created",
      "conversation_id": "CONVERSATION_ID",
      "external_user_id": "customer_123",
      "sequence": 2,
      "request_id": "9bfb0e4f-8b67-4f6d-9d34-44e8b3ac6ae2",
      "occurred_at": "2026-10-14T13:30:00+08:00",
      "data": {
        "message_id": 8001,
        "sender": "bot",
        "message": {
          "type": "text",
          "content": "Hello, how may I assist you?"
        }
      }
    }
  ]
}
```

*The above is sample data, not a real customer or conversation.*

Please process events in `sequence` order, and store the last successfully processed sequence number for use in the next query. Events may skip some sequence numbers depending on subscription settings,**you do not need to wait for consecutive numbers**. Polling returns the currently available events immediately and will not keep waiting for new events.

## 4. Event Types

Webhook and event polling use the same single-event data format.

| Event                               | Description                                                     |
| ----------------------------------- | --------------------------------------------------------------- |
| `message.received`                  | Customer message received.                                      |
| `message.created`                   | A message to be sent to the customer is generated.              |
| `turn.completed`                    | Processing of this round of customer messages is complete.      |
| `conversation.transferred_to_human` | Conversation transferred to the human customer service process. |
| `conversation.ended`                | Conversation has ended.                                         |
| `tool.invoking`                     | AI tool starts executing.                                       |
| `tool.completed`                    | AI tool finished executing.                                     |

You can choose which events to receive in the pipeline's Webhook settings. By default, it includes `message.created`、`turn.completed`、`conversation.transferred_to_human` and `conversation.ended`;**event polling also filters by the same subscription list**.

### message events

`message.created` of `data` Example:

```json
{
  "message_id": 8001,
  "sender": "bot",
  "message": {
    "type": "text",
    "content": "Hello, how may I help you?"
  }
}
```

`sender` May be:

* `bot`: automatic replies, including AI agents, conversation scripts, or system messages.
* `human`: message sent by a human customer service agent.

Message types may include `text`、`image`、`quickreply`、`button`、`carousel`;`content` content varies by type. Please render according to `message.type` and provide appropriate fallback display for unrecognized types.

### Turn completed event

`turn.completed` of `data` May include:

```json
{
  "in_reply_to": [
    "9bfb0e4f-8b67-4f6d-9d34-44e8b3ac6ae2"
  ],
  "unfinished_tool_call_ids": []
}
```

`in_reply_to` is the list of request IDs processed in this turn. Please note:**Turn completion does not mean the entire conversation has ended**. If you want to determine whether the conversation has formally ended, refer to `conversation.ended` and other events.

## 5. Receive Webhook

In FIRST LINE's custom pipeline settings, enter a publicly accessible **HTTPS** Webhook URL, and select the events you want to subscribe to. FIRST LINE sends event JSON via HTTP POST.

The Webhook includes the following headers:

| Header                  | Description                     |
| ----------------------- | ------------------------------- |
| `X-FirstLine-Event`     | Event type.                     |
| `X-FirstLine-Event-Id`  | Event identifier.               |
| `X-FirstLine-Timestamp` | Unix Timestamp (seconds).       |
| `X-FirstLine-Signature` | Webhook HMAC SHA-256 signature. |

### Verify Webhook Signature

To confirm the request comes from the expected source, use the pipeline's **Webhook Secret** verify signature:

```
signed_content = timestamp + "." + raw_request_body
expected_signature = "sha256=" + HMAC_SHA256(webhook_secret, signed_content).hex()
```

You must use the received**raw HTTP Request Body** for calculation; do not decode the JSON and re-encode it first.

Node.js verification calculation example:

```javascript
import { createHmac, timingSafeEqual } from 'node:crypto'

function verifyWebhook(rawBody, timestamp, signature, secret) {
  if (!/^\d+$/.test(timestamp) || !/^sha256=[0-9a-f]{64}$/.test(signature)) {
    return false
  }

  // It is recommended to check the time difference to reduce replay request risk
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
    return false
  }

  const expected = 'sha256=' + createHmac('sha256', secret)
    .update(timestamp + '.')
    .update(rawBody)
    .digest('hex')

  return timingSafeEqual(Buffer.from(expected), Buffer.from(signature))
}
```

After verification is complete, use `event_id` for deduplication, then let the enterprise system process the event. The receiver should return 2xx as soon as possible to avoid timeouts caused by long-running tasks. Webhooks may be resent,**do not rely on notifications being delivered only once or always in order**.

### when push delivery fails

If the Webhook has a temporary connection problem, or the receiver responds with `408`、`429`、`5xx`, the system may retry automatically. Other `4xx` responses are usually not retried. It is recommended that the integration side keep event processing records and verify them with the event query API when necessary.

{% hint style="warning" %}
After regenerating the Webhook Secret, the old key will become invalid. Please update the enterprise-side signature verification settings accordingly to avoid being unable to receive subsequent notifications.
{% endhint %}

## 6. Context Data Context (Optional)

When sending a message, you may provide auxiliary data related to this service, for example:

```json
{
  "context": {
    "membership_level": "gold",
    "preferred_language": "zh_tw"
  }
}
```

Restrictions are as follows:

* Up to 20 key-value pairs.
* Keys are limited to lowercase English letters, numbers, and underscores, 1 to 40 characters in length.
* Values must be strings, up to 500 characters.
* If the limit is exceeded, `422`will be returned instead of truncating the content.

Context can be referenced by service flows and AI agents, but should not be used to transmit passwords, access tokens, or other unnecessary sensitive information. Enterprises should first verify the data source to prevent customers from forging membership identity or service permissions.

## 7. Common Errors and Handling

| Status Code | Condition                                  | Recommended Handling                                                        |
| ----------- | ------------------------------------------ | --------------------------------------------------------------------------- |
| `200`       | Event query succeeded.                     | Process the retrieved events in order.                                      |
| `202`       | Customer message received.                 | Wait for the Webhook or query subsequent events.                            |
| `401`       | Invalid access token or pipeline status.   | Check the Token, Client credentials, and pipeline activation status.        |
| `403`       | Insufficient access permissions.           | Confirm that the Token has `headless:use` scope.                            |
| `404`       | No accessible conversation found.          | Confirm `conversation_id` and pipeline.                                     |
| `409`       | The same message is still being processed. | Retry later with the same ID and content.                                   |
| `422`       | Field format does not match.               | Correct the request data.                                                   |
| `429`       | Requests are too frequent.                 | Reduce the frequency and use backoff retries.                               |
| `5xx`       | Temporary service anomaly.                 | Log the error and retry a limited number of times with the same message ID. |

{% hint style="info" %}
Event data has a retention period. For long-term retention needs, the enterprise should plan according to its own policy. When tracking a conversation for a long time, it is recommended to properly save received events and processing records.
{% endhint %}

## Pre-launch checks

* A custom pipeline has been created and activated, and the receiver settings are correct.
* The Access Token can be successfully authorized, and credentials are stored only in the backend.
* Text messages can be sent and HTTP 202 can be received.
* Replies can be received via Webhook or event polling.
* Event deduplication, message retries, and asynchronous replies have been handled.
* Human handoff, conversation ending, and common error scenarios have been tested.

**After completing the above steps, you can integrate FIRST LINE's conversation capabilities into your enterprise's own service interface.**
