LINE Messaging API SDK for Elixir
Introduction
The LINE Messaging API SDK for Elixir makes it easy to develop bots using the LINE Messaging API, and you can create a sample bot within minutes.
The API clients in this SDK are generated from the line-openapi specification published by LINE, so they track the official API surface.
Note
This is a community-maintained SDK. It is not an official LINE product.
Documentation
See the official API documentation for more information.
- English: https://developers.line.biz/en/docs/messaging-api/overview/
- Japanese: https://developers.line.biz/ja/docs/messaging-api/overview/
Module reference for this SDK is published on HexDocs.
Requirements
This library requires Elixir 1.18 or later.
Installation
Add :line_bot_sdk to the dependencies in your mix.exs:
def deps do
[
{:line_bot_sdk, "~> 0.1"}
]
end
Then fetch it:
$ mix deps.get
Configuration
Every API client is built with new/1, which returns a Req.Request struct that you
pass to each API call. Keep your channel access token and channel secret out of the
source tree — read them from the environment or from application config.
client =
LINE.Bot.MessagingApi.new(
channel_token: System.fetch_env!("LINE_CHANNEL_TOKEN")
)
new/1 accepts:
:channel_token— channel access token used for bearer authorization (required by every client exceptLINE.Bot.ChannelAccessToken, which issues tokens).:base_url— base URL for requests. Each client has its own default, listed under API clients.:plug— a plug to serve the requests instead of the network, for testing. See Testing.
Every API call takes a final keyword list that is merged into the underlying
Req.request/2 call, so you can set :receive_timeout, :retry or anything else Req
understands on a per-request basis:
LINE.Bot.MessagingApi.push_message(client, request, [], receive_timeout: 10_000)
API clients
| Module | API | Default base URL |
|---|---|---|
LINE.Bot.MessagingApi | Messaging API | https://api.line.me |
LINE.Bot.MessagingApiBlob | Messaging API endpoints that transfer binary content | https://api.line.me (see note) |
LINE.Bot.ChannelAccessToken | Channel access token issuing and revoking | https://api.line.me |
LINE.Bot.Insight | Insight API | https://api.line.me |
LINE.Bot.Liff | LIFF server API | https://api.line.me |
LINE.Bot.ManageAudience | Audience management | https://api.line.me |
LINE.Bot.ManageAudienceBlob | Audience management file uploads | https://api.line.me (see note) |
LINE.Bot.LineModule | Module API | https://api.line.me |
LINE.Bot.LineModuleAttach | Module attach API | https://manager.line.biz |
LINE.Bot.Shop | Shop API | https://api.line.me |
Important
LINE serves the blob endpoints from https://api-data.line.me, but the generated
clients do not yet pick up that per-operation host. Pass it explicitly:
LINE.Bot.MessagingApiBlob.new(
base_url: "https://api-data.line.me",
channel_token: System.fetch_env!("LINE_CHANNEL_TOKEN")
)
Request and response bodies are plain structs under each client's Model namespace, for
example LINE.Bot.MessagingApi.Model.TextMessage. Webhook event structs live under
LINE.Bot.Webhook.Model.
Getting started
The snippet below is a complete echo bot: it verifies the webhook signature, decodes the events and replies to every text message with the same text.
defmodule MyBot.Router do
use Plug.Router
alias LINE.Bot.MessagingApi
alias LINE.Bot.MessagingApi.Model.ReplyMessageRequest
alias LINE.Bot.MessagingApi.Model.TextMessage
alias LINE.Bot.Webhook.Model.MessageEvent
alias LINE.Bot.Webhook.Model.TextMessageContent
plug :match
plug LINE.Bot.Webhook.Plug, channel_secret: {System, :fetch_env!, ["LINE_CHANNEL_SECRET"]}
plug :dispatch
post "/callback" do
client = MessagingApi.new(channel_token: System.fetch_env!("LINE_CHANNEL_TOKEN"))
Enum.each(conn.assigns.webhook_payload.events, &handle_event(client, &1))
send_resp(conn, 200, "OK")
end
match _ do
send_resp(conn, 404, "Not Found")
end
# Echo any text message back to the sender.
defp handle_event(client, %MessageEvent{
replyToken: reply_token,
message: %TextMessageContent{text: text}
})
when is_binary(reply_token) do
MessagingApi.reply_message(client, %ReplyMessageRequest{
replyToken: reply_token,
messages: [%TextMessage{type: "text", text: text}]
})
end
defp handle_event(_client, _event), do: :ok
end
Webhook
The plug
LINE.Bot.Webhook.Plug verifies the x-line-signature header, decodes the request body
and assigns the parsed LINE.Bot.Webhook.Model.CallbackRequest struct to
conn.assigns.webhook_payload. A request whose signature is missing or does not match
gets a 401 response and the connection is halted, so your handler only ever sees
verified payloads.
plug LINE.Bot.Webhook.Plug, channel_secret: {System, :fetch_env!, ["LINE_CHANNEL_SECRET"]}
:channel_secret is required and accepts either a plain string, which is resolved when
the plug pipeline is compiled, or an {module, function, arguments} tuple, which is
resolved on every request. Prefer the tuple form so the secret is read at runtime rather
than baked into the compiled module.
The plug reads the raw request body itself, because the signature is computed over the
bytes exactly as they were sent. Do not run Plug.Parsers on the webhook path ahead of
it, or the body will already be consumed and verification will fail. The decoded JSON map
is still available afterwards as conn.body_params if you need the untyped payload.
The payload
conn.assigns.webhook_payload is a CallbackRequest with two fields:
destination— the user ID of the bot that should receive the events.events— a list of event structs.
Fields keep the casing used by the LINE API, so they are replyToken and
webhookEventId, not reply_token and webhook_event_id.
Events
Each entry in events is decoded into a concrete struct under
LINE.Bot.Webhook.Model based on its type, so you can dispatch with pattern matching:
type | Struct |
|---|---|
message | MessageEvent |
messageEdited | MessageEditedEvent |
unsend | UnsendEvent |
follow | FollowEvent |
unfollow | UnfollowEvent |
join | JoinEvent |
leave | LeaveEvent |
memberJoined | MemberJoinedEvent |
memberLeft | MemberLeftEvent |
membership | MembershipEvent |
postback | PostbackEvent |
videoPlayComplete | VideoPlayCompleteEvent |
beacon | BeaconEvent |
accountLink | AccountLinkEvent |
delivery | PnpDeliveryCompletionEvent |
module | ModuleEvent |
activated | ActivatedEvent |
deactivated | DeactivatedEvent |
botSuspended | BotSuspendedEvent |
botResumed | BotResumedEvent |
An event type this SDK does not know about — a new one added to the platform, for
instance — decodes into the base LINE.Bot.Webhook.Model.Event struct rather than
raising, so a catch-all clause keeps your bot running:
defp handle_event(_client, %Event{type: type}) do
Logger.info("ignoring unhandled event type #{type}")
end
The source of an event is likewise one of UserSource, GroupSource or RoomSource,
and the message of a MessageEvent is one of TextMessageContent,
ImageMessageContent, VideoMessageContent, AudioMessageContent,
FileMessageContent, LocationMessageContent or StickerMessageContent. Matching on
both at once is a compact way to route:
defp handle_event(client, %MessageEvent{
source: %UserSource{userId: user_id},
message: %StickerMessageContent{stickerId: sticker_id}
}) do
...
end
Verifying a signature yourself
If you receive the request body somewhere other than a plug pipeline, verify it directly. Pass the raw bytes, not a re-encoded map:
LINE.Bot.Webhook.signature_valid?(channel_secret, signature, raw_body)
It returns a boolean and compares in constant time.
Calling the API
Sending a message
alias LINE.Bot.MessagingApi
alias LINE.Bot.MessagingApi.Model.PushMessageRequest
alias LINE.Bot.MessagingApi.Model.TextMessage
client = MessagingApi.new(channel_token: System.fetch_env!("LINE_CHANNEL_TOKEN"))
request = %PushMessageRequest{
to: "U4af4980629...",
messages: [%TextMessage{type: "text", text: "Hello, world"}]
}
{:ok, response} = MessagingApi.push_message(client, request)
Optional headers such as the retry key are passed through the optional-arguments keyword list:
MessagingApi.push_message(client, request,
x_line_retry_key: "123e4567-e89b-12d3-a456-426614174000"
)
Handling the response
Each API call decodes the response according to the statuses the OpenAPI specification
documents for that endpoint. A documented status — including a documented error status
such as 400 or 429 — returns {:ok, struct}, where the struct is the model declared
for it. Match on the struct type to tell success from a documented failure:
alias LINE.Bot.MessagingApi.Model.ErrorResponse
alias LINE.Bot.MessagingApi.Model.PushMessageResponse
case MessagingApi.push_message(client, request) do
{:ok, %PushMessageResponse{} = response} ->
response.sentMessages
{:ok, %ErrorResponse{} = error} ->
Logger.warning("LINE rejected the request: #{error.message} #{inspect(error.details)}")
{:error, %Req.Response{} = response} ->
Logger.error("unexpected status #{response.status}: #{inspect(response.body)}")
{:error, exception} ->
Logger.error("request failed: #{Exception.message(exception)}")
end
Endpoints with an empty body return {:ok, nil}. An undocumented status returns
{:error, %Req.Response{}}, from which you can read the status, headers and raw body:
{:error, response} = MessagingApi.push_message(client, request)
response.status
Req.Response.get_header(response, "x-line-request-id")
A transport-level failure returns {:error, exception} as raised by Req.
Testing
Pass :plug to new/1 to serve requests from your own plug instead of the network,
which makes it possible to test bot code without reaching LINE:
client =
LINE.Bot.MessagingApi.new(
channel_token: "test-channel-token",
plug: fn conn ->
Req.Test.json(conn, %{"sentMessages" => []})
end
)
Alternatively point :base_url at a local mock server.
Development
Requirements
Working on the SDK itself needs more than working on an app that depends on it, because the API clients are generated by a custom openapi-generator plugin written in Java:
- Elixir 1.18 or later — the SDK and the
generate-code.exsscript. - JDK 18 or later — the generator plugin targets Java 18, and CI builds it with 21.
- Apache Maven —
generate-code.exsbuilds the plugin withmvn package. Amvnwwrapper is checked in, but it has no.mvn/wrapperconfiguration and the script callsmvndirectly, so install Maven on yourPATH.
The OpenAPI specifications come from the line-openapi git submodule, so clone with submodules or initialize them afterwards:
$ git submodule update --init --recursive
Regenerating the clients
The API clients under lib/gen are generated and must not be edited by hand — any change
there is lost on the next run. Change the templates and generator code in generator/
instead, then regenerate:
$ elixir generate-code.exs
$ mix compile
$ mix test
generate-code.exs builds the generator with Maven, deletes lib/gen, regenerates every
client from the specs, and runs mix format over the result. Commit the regenerated code
together with the generator/ change: CI regenerates and fails the build if the working
tree comes out dirty.
Running the test suite
$ mix deps.get
$ mix test
The tests do not reach the network. Each client is exercised against a local mock server
through LINE.Bot.ClientCase, which builds a client pointed at it.
Help and media
FAQ: https://developers.line.biz/en/faq/
News: https://developers.line.biz/en/news/
Versioning
This project respects semantic versioning.
While the version is below 1.0.0, minor releases may contain breaking changes as the
generated API surface stabilizes. Notable changes are recorded in the changelog.
Contributing
Issues and pull requests are welcome — see the issue tracker.
Before opening a pull request, please read Development above. The most
common pitfall is editing lib/gen directly: that code is generated, so changes there
belong in generator/ and must be regenerated and committed together.
License
Copyright 2026 Thanabodee Charoenpiriyakij
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.