> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# LINE onboarding: Messaging API channel to working two-way chat

> Walk the LINE channel end to end — connect a Messaging API channel to Orbit, send your first push through POST /api/v1/messages/line, and follow how inbound events resolve to your tenant from the LINE channel id (destination) on every webhook.

# LINE Onboarding: Messaging API Channel to Working Two-Way Chat

This guide walks you from an empty LINE channel to a working, production-ready setup. It complements the [LINE channel page](/channels/line), which covers message shapes and capability detail — this guide is the ordered path you follow the first time, and re-run whenever you attach, rotate, or retire a LINE Official Account.

LINE has no template approval cycle for plain text, and no brand gate beyond having a LINE Official Account. The real work is connecting the right credentials and understanding which LINE recipient ids are usable. The [Asia channels onboarding guide](/guides/asia-channels-onboarding) positions LINE beside KakaoTalk, Viber, and WeChat; this guide is the LINE-specific walkthrough.

## 1. Prerequisites — the Messaging API channel

You need two things from a LINE Official Account before Orbit can carry traffic:

1. A **LINE Official Account**. If you do not have one, create it in the [LINE Official Account Manager](https://manager.line.biz/).
2. A **Messaging API channel** on that account, created in the [LINE Developers Console](https://developers.line.biz/console/).

From the channel you need two credentials — copy them once, because the console does not always show the secret again:

* **Channel access token (long-lived)** — from the channel's **Messaging API** tab. This is the bearer token LINE accepts on outbound push calls.
* **Channel secret** — from the channel's **Basic settings** tab. Orbit uses it to verify the `x-line-signature` header on inbound webhooks before any event reaches your handler.

Also copy the numeric **channel id** (from the **Basic settings** tab) — it is the `destination` value every inbound webhook carries, and it is what identifies your tenant on the shared inbound URL.

Treat both values as bearer credentials: anyone holding them can message as your Official Account. See [Credential handling — your responsibility](/channels/line#credential-handling-your-responsibility) on the channel page for the handling rules (never commit to git, scope dev/staging/production to separate channels, rotate on suspicion).

## 2. Connect the channel to Orbit

Paste the two values once in the dashboard:

1. Navigate to **Channels → LINE → Connect**.
2. Paste the **channel access token** and **channel secret**. Orbit validates the token at connect time — an expired or invalid token returns `LINE_INVALID_TOKEN` (401) instead of connecting.
3. Orbit stores both values encrypted at rest under your organization's `settings.channels.line` and never echoes them back through any API response.
4. In the LINE Developers Console, set the channel's **Webhook URL** to `https://api.orbit.devotel.io/api/v1/webhooks/inbound/line` and enable **Use webhook**. Every Orbit tenant shares this single URL — the LINE channel id (`destination`) on each inbound event is what resolves the receiving tenant (see §4).

Connect and disconnect are write-class operations — they require the `owner`, `admin`, or `developer` role.

## 3. Send — POST /api/v1/messages/line

Sending goes through the unified Messaging surface. The text body is the top-level `body` field, the LINE message type is the top-level `type` field (defaulting to `text`), and the per-type secondary options ride under `metadata` as string→string values. The sender is selected automatically from your connected channel credentials — you never pass a token or channel id on the request.

The request body is the shared free-form shape used by the other OTT channels (Viber, Telegram, KakaoTalk) — `to` + `body` are the only required fields, with optional `from`, `media_url`, `metadata`, and `scheduled_at`:

| Field      | Required | Notes                                                                                                                                 |
| ---------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `to`       | yes      | Recipient LINE id — a user id (`U…`), group id (`C…`), or room id (`R…`) — taken from an inbound event.                               |
| `body`     | yes      | Message body, 1–5000 characters.                                                                                                      |
| `type`     | no       | One of `text`, `image`, `video`, `audio`, `location`, `sticker`, `flex`. Defaults to `text`.                                          |
| `metadata` | no       | Flat string→string map. Non-text types read their options here (see [Request body](/channels/line#request-body) on the channel page). |

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST https://api.orbit.devotel.io/api/v1/messages/line \
    -H "X-API-Key: dv_live_sk_..." \
    -H "Content-Type: application/json" \
    -d '{
      "to": "U4af4980629...",
      "body": "Welcome! Reply with a number to continue.",
      "type": "text"
    }'
  ```

  ```typescript Node.js theme={null}
  import { Devotel } from '@devotel-orbit/node';

  const orbit = new Devotel({ apiKey: process.env.ORBIT_API_KEY });

  // Fully-typed per-channel method — prefer it over the generic send.
  // LINE accepts up to 5 stacked messages per push.
  const message = await orbit.messages.sendLine({
    to: 'U4af4980629...',
    messages: [
      { type: 'text', text: 'Welcome! Reply with a number to continue.' },
    ],
  });

  console.log(message.data.id);      // msg_line_abc123
  console.log(message.data.status);  // 'queued'
  ```

  ```python Python theme={null}
  from orbit_sdk import OrbitClient

  client = OrbitClient.from_env()  # reads ORBIT_API_KEY

  # The Python SDK has no typed send_line method — reach the channel
  # endpoint through the client.request escape hatch (raw JSON returned).
  message = client.request(
      "POST",
      "/messages/line",
      json_body={
          "to": "U4af4980629...",
          "body": "Welcome! Reply with a number to continue.",
          "type": "text",
      },
  )

  print(message["data"]["id"])       # msg_line_abc123
  print(message["data"]["status"])   # 'queued'
  ```
</CodeGroup>

`202 Accepted` means queued — the terminal `delivered` / `failed` state arrives later on the delivery-status webhook, whose envelope shape is documented on the [channel page](/channels/line#delivery-status-envelope-samples). A soft failure (recipient blocked your Official Account) shows up there as `message.failed` with `error_code: MESSAGE_DELIVERY_FAILED`.

Outbound `POST /messages/line` is capped at **80 requests/minute per tenant**; bursts past the cap receive `429` with a `Retry-After` header, so stage high-volume sends through the campaigns API.

## 4. Inbound — tenant resolution from `destination`

LINE fans every tenant's inbound traffic into the single platform URL you set in §2. Each inbound event carries a `destination` field — the numeric LINE channel id of the receiving Official Account — and Orbit resolves the owning tenant from that value before doing anything else:

1. Read the untrusted payload's `destination` and resolve the tenant that connected that channel id.
2. Verify the `x-line-signature` HMAC-SHA256 header against that tenant's stored channel secret. Requests that fail verification are rejected before any webhook fires.
3. Normalize each `message`-type event into the [canonical inbound envelope](/webhooks/normalized-inbound-envelope) and forward it to the endpoint configured under **Settings → Webhooks**, signed with the Standard-Webhooks scheme.

Because tenant resolution happens per-event, the shared URL is multi-tenant-safe: a misconfigured channel id cannot route traffic into a neighbor's webhook chain.

Inbound **user messages** are the only events that reach your webhook receiver. Follows (joins), unfollows, postbacks (button taps), and reads arrive in the same verified payload, but the dispatcher acknowledges and drops them — if your flow must react to a button press, run it inside Orbit (flows, journeys), not in your receiver. The sender's LINE id (`U…`) on the envelope is the `to` value for every outbound reply.

An inbound text message arrives like this:

```json theme={null}
{
  "id": "evt_9d4e2b8c1f",
  "type": "message.received",
  "created_at": "2026-09-02T08:31:12Z",
  "data": {
    "message_id": "msg_1n58gk",
    "channel": "line",
    "from": "U4af4980629...",
    "to": "1654385910",
    "body": "Store hours?",
    "provider_status": "inbound"
  }
}
```

Delivery receipts from LINE advance the outbound lifecycle on a separate `POST /api/v1/webhooks/dlr/line` route that Orbit registers as part of connect — nothing for you to wire. Full envelope samples are on the [channel page](/channels/line#inbound-envelope-samples).

## 5. Verify the loop end to end

1. From a LINE device, message your Official Account — confirm a `message.received` event lands on your webhook within seconds.
2. Reply through `POST /messages/line` using the sender's `U…` id from that event — confirm the message arrives in the chat.
3. Confirm the `message.delivered` event arrives and that the message record in the dashboard moved through `queued → delivered`.

If inbound never arrives, check the LINE Developers Console webhook section: **Use webhook** must be enabled and the URL must be exactly `https://api.orbit.devotel.io/api/v1/webhooks/inbound/line`. LINE also enforces that webhook URL validation responds on the same domain — if you rotate credentials, re-paste them in Orbit rather than re-pointing the URL.

## 6. Rotation and retirement

Rotating the access token from the LINE Developers Console does not break history — Orbit keys conversation history by channel id, not by token, and re-pasting the new token value under **Channels → LINE** preserves everything. Disconnecting the channel in the dashboard removes the credential row; inbound webhook traffic for that channel id then resolves to no tenant and is rejected, so retire it only after the Official Account has stopped receiving traffic.

## 7. Error reference

A failed send returns a JSON error envelope whose `error.code` is one of the values below.

| Code                  | HTTP | Cause                                                                                                            | Fix                                                                                                                                             |
| --------------------- | ---- | ---------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `VALIDATION_ERROR`    | 422  | `to` or `body` missing, `body` exceeds 5000 characters, or a `flex` message's `flex_contents` is not valid JSON. | Correct the request body and retry.                                                                                                             |
| `INVALID_RECIPIENT`   | 422  | `to` is empty or is not a usable LINE recipient id.                                                              | Reply to a user (`U…`), group (`C…`), or room (`R…`) id taken from an inbound event.                                                            |
| `MESSAGE_SEND_FAILED` | 502  | LINE's Messaging API rejected the push, or a token rotated after connect.                                        | For a token rotation, re-paste the current token under **Channels → LINE**. Otherwise check the failure reason on the status webhook and retry. |
| `RATE_LIMITED`        | 429  | More than 80 sends/minute for the tenant.                                                                        | Honour the `Retry-After` header; stage bulk sends through the campaigns API.                                                                    |

`LINE_INVALID_TOKEN` (401) is returned only at **connect** time, for an expired or invalid channel access token; it is never raised on the send path.

## Compliance scope

LINE does not impose platform-side template or quiet-hours gates on plain text, and Orbit does not either — any consent, opt-out, or timing policy you promise your LINE audience is enforced from your own tenant configuration (quiet hours, frequency caps, suppression lists), not by this channel's connect path. Keep those controls enabled in production and follow them in your journey logic; this guide only wires the transport.

## Where to go next

* [LINE channel page](/channels/line) — message types, capability detail, pricing notes.
* [Asia channels onboarding](/guides/asia-channels-onboarding) — LINE beside KakaoTalk, Viber, and WeChat.
* [API recipes cookbook](/guides/api-recipes) — the send → poll → webhook loop task by task, with curl and per-language SDK snippets.
* [Inbound webhook debugging](/guides/inbound-webhook-debugging) — tools for the receiver side on any channel.
