LINE Webhook Gateway
Overview
POST /api/line/:id is the single endpoint that the LINE Platform calls directly whenever
an event occurs on a LINE Official Account — messages, button taps, follows, blocks,
group joins and leaves, beacons, and so on. The :id path parameter is the webhook id
bound to one specific LINE OA (the line_oa.webhook_id column).
One thing to understand before reading further: this service does not implement a separate handler per event type. It acts as a thin router and filter that asks only three questions before delegating the work elsewhere:
- Does this OA need its raw webhook forwarded to a customer endpoint?
- Does this event match the mbox (human agent handoff) conditions or a special postback?
- If neither applies, publish the whole payload to the
line_webhookqueue and let worker-go sort it out.
As a result, events such as follow, unfollow, join, leave, beacon, and
videoPlayComplete have no handling code in this project at all. They all fall through to the
default path into the line_webhook queue. The real logic for auto-response, friend tracking,
and the line_user_friend table lives entirely in worker-go.
The endpoint is fire-and-forget: it immediately returns 200 with
{"code":"RES_SUCCESS_001","message":"OK"} to LINE, then processes the event in a background
goroutine. LINE enforces a short timeout and retries on slow responses, so responding first is
mandatory.
Business Flow
Request intake (handler)
- Read
:idaswebhookId. - Collect every header and lowercase the keys. LINE and Express send them lowercase, but Go
canonicalises them, so they must be converted back — otherwise the
x-line-signaturelookup fails. - Read the body while also retaining the raw bytes in
BodyRaw, separate from the parsed map (see Forwarding the Raw Body and Signature). - Spawn a goroutine calling
Service.ProcessLineoncontext.Background(), guarded byrecover()so a panic cannot take down the process, then return200immediately.
Processing (ProcessLine)
- Load the config with
HGETALL webhook_config:{webhookId}from Redis (see Redis Cache for Webhook Config).- On a cache miss (empty hash), publish the entire payload to
line_webhookand stop. worker-go will look the config up from the database itself. - On a read error, log and stop immediately without publishing. The message is lost — this matches the original behaviour.
- On a cache miss (empty hash), publish the entire payload to
- If the config contains
forwardWebhookUrl, fire the request onward without waiting for a result (see Forwarding Webhooks to Customer Systems). - Filter events where
type == "message". IfmboxEnabled == "1"and a message event exists, enter the mbox path (see Handoff to Human Agents). When this path matches it returns immediately without publishing toline_webhook, which effectively silences the bot. - Filter events where
type == "postback"whenmboxEnabled == "1".postback.datastarting withmbox_team:triggers a handoff to the selected team.postback.datastarting withappt_cancel:triggers appointment cancellation (see Cancelling Appointments via Postback).
- Default path: publish a payload of
webhookId,headers, andbodyto theline_webhookqueue as bare JSON for worker-go to run the bot logic.
Steps 7 and 8 inspect only the first event of each type (messageEvents[0] and
postbackEvents[0]), even though LINE may deliver multiple events per request. This preserves
parity with the original NestJS implementation. Also, once mbox decides to hand off, every
other event in the same batch is discarded.
Key Files & Functions
| Method | Route | Auth | Handler |
|---|---|---|---|
| POST | /api/line/:id | None | Handler.processLine |
| POST | /api/Line/:id | None | Handler.processLine (same handler) |
Both spellings are registered because NestJS and Express match paths case-insensitively (the
original declared @Controller('Line')), whereas gin is case-sensitive and real LINE traffic
arrives lowercase.
All code lives in the internal/line/ package.
| File | Key functions |
|---|---|
internal/line/handler.go | Register, Handler.processLine, toLowerHeader, decodeJSONObject |
internal/line/service.go | Service.ProcessLine (the main dispatcher), eventList, eventType, sourceUserID, messageText, postbackData, Service.publish, parseLineOaID, splitSecond |
The key type is LineWebhookPayload, holding webhookId, headers, and body, with a custom
MarshalJSON that emits the raw bytes as the body field.
Connections to Other Services
- Redis — reads
webhook_config:{webhookId}(read-only) and reads/writesagent_mode:{lineOaId}:{userId}throughinternal/cache.Cache, which automatically prefixes keys withLINE_MANAGEMENT:. - RabbitMQ — the
line_exchangeexchange with theline_webhook,mbox_handoff, andbooking_notificationqueues, using the queue name as the routing key and bare JSON bodies. - No database access — the
linemodule never touches PostgreSQL; everything comes from the Redis cache. - worker-go — consumes
line_webhookand performs auto-response, friend tracking, and writes to theline_userandline_user_friendtables. - cms-api — its line-oa-management module writes
webhook_config:*into Redis whenever an OA's configuration changes.