ExLine

An Elixir client for the LINE platform — Messaging API today, LIFF / LINE Login planned.

Unofficial. Not affiliated with or endorsed by LY Corporation.

Design

Installation

def deps do
[
{:ex_line, "~> 0.1.0"}
]
end

:plug is an optional dependency, only needed if you use ExLine.Webhook.Plug / ExLine.Webhook.BodyReader.

Configuration

You need credentials from the LINE Developers Console:

CredentialWhere it's usedFrom
Channel access tokensending messages (ExLine.Client)Messaging API channel
Channel secretwebhook signature (ExLine.Webhook)Messaging API channel

ExLine never holds global credential state — you pass what each call needs. There are three ways to supply them, pick per use case:

1. Per-call value (default; multi-channel / multi-tenant friendly). Build a client from wherever you store the token (DB row, etc.) and pass it in:

client = ExLine.Client.new(access_token: channel.access_token)
ExLine.Api.Messaging.push(client, user_id, message)

2. From application config (single-channel convenience).

# config/runtime.exs
config :ex_line,
access_token: System.fetch_env!("LINE_CHANNEL_ACCESS_TOKEN"),
channel_id: System.get_env("LINE_CHANNEL_ID")
client = ExLine.Client.from_env()

3. Webhook secret via a resolver (kept separate from the client). The channel secret belongs to a different trust boundary, so it is passed directly — as a static value, or a fn conn -> secret end resolver that picks the right channel at request time (see Receiving webhooks):

plug ExLine.Webhook.Plug, secret: System.fetch_env!("LINE_CHANNEL_SECRET")
# or, multi-channel:
plug ExLine.Webhook.Plug, secret: fn conn -> MyApp.secret_for(conn) end

Never commit tokens or secrets — load them from the environment.

Sending messages

client = ExLine.Client.new(access_token: "CHANNEL_ACCESS_TOKEN")
# push
ExLine.Api.Messaging.push(client, "U123...", ExLine.Message.text("hello"))
# reply (using a webhook replyToken)
ExLine.Api.Messaging.reply(client, reply_token, [
ExLine.Message.text("hi"),
ExLine.Message.Template.buttons("Pick one", [
ExLine.Message.Action.message("A", "a"),
ExLine.Message.Action.postback("B", "action=b")
])
])

Push supports idempotent retries via X-Line-Retry-Key:

ExLine.Api.Messaging.push(client, "U123...", msg, retry_key: "a-uuid")

Errors come back as {:error, %ExLine.Error{kind: kind}} where kind is one of :transient, :quota_exceeded, :permanent, or :network (see ExLine.Error.retryable?/1).

Receiving webhooks

Verify the signature (works with or without Plug):

ExLine.Webhook.Signature.valid?(raw_body, signature, channel_secret)

With Plug, preserve the raw body in your parser, then verify in the pipeline. The :secret option takes a static binary or a fn conn -> secret end resolver so you can pick the right channel per request:

plug Plug.Parsers,
parsers: [:json],
body_reader: {ExLine.Webhook.BodyReader, :read_body, []},
json_decoder: Jason
plug ExLine.Webhook.Plug, secret: &MyApp.line_secret/1

Routing events

defmodule MyApp.LineRouter do
use ExLine.EventRouter
text "hello", MyApp.HelpHandler, :hello
postback "buy", MyApp.ShopHandler, :buy
follow MyApp.OnboardHandler, :welcome
default MyApp.FallbackHandler, :unknown
@impl true
def before_action(event, assigns), do: {event, Map.put(assigns, :client, MyApp.client())}
end
defmodule MyApp.HelpHandler do
use ExLine.EventHandler
@impl true
def handle_event(:hello, %{"replyToken" => token}, %{client: client}) do
ExLine.Api.Messaging.reply(client, token, text("Need help?"))
:ok
end
end
# in your webhook controller, for each event:
MyApp.LineRouter.call(event, %{})

Testing

Mock the adapter to assert outbound requests without hitting the network:

# test_helper.exs
Mox.defmock(MyApp.LineAdapterMock, for: ExLine.Client.Adapter)
# in a test
client = ExLine.Client.new(access_token: "tok", adapter: MyApp.LineAdapterMock)
Mox.expect(MyApp.LineAdapterMock, :request, fn req ->
assert req.url == "https://api.line.me/v2/bot/message/push"
{:ok, %{status: 200, body: %{}}}
end)
ExLine.Api.Messaging.push(client, "U1", ExLine.Message.text("hi"))

Status

Early. Implemented: client + adapter, message builders (text / sticker / buttons / confirm + actions), Messaging.reply / push, webhook signature verification + Plug, and the event routing DSL. Broader Messaging coverage (multicast / broadcast / rich menu / content) and LIFF support are planned — see notes/.