Skip to main content

Receiving Chatwoot Callbacks

Overview

POST /api/mbox/callback/:oaHash is the return path of the live chat system — the endpoint Chatwoot calls into us whenever something happens on the agent's side: a reply is typed, a case is opened or closed, or contact details are edited. It lets our system push messages back to the LINE user and keep status in sync.

Chatwoot emits a large volume of events, many of which are irrelevant to us — chats from other channels, or inbound messages we pushed in ourselves. This handler is therefore purely a filter: any event that fails the criteria gets {"status":"skipped"} and is dropped without being published.

The :oaHash parameter identifies the LINE OA (the line_oa.line_oa_hash column). The handler does not use it for any decision; it simply attaches it to the payload as an _oaHash field for worker-go to resolve later.

Unlike the LINE endpoint, this one is not fire-and-forget. It waits for the publish to succeed before responding, so Chatwoot can retry if our side fails.

Business Flow

  1. Read :oaHash from the path.
  2. Read the body and parse it as JSON. If it cannot be read or is not JSON, respond 200 with {"status":"skipped"}.
  3. Filter on body.event, accepting only these five types:
EventRequired conditions
message_createdThe channel must be Channel::Line and message_type must be "outgoing" or 1, meaning a message sent by an agent
conversation_createdThe channel must be Channel::Line, read from body.channel first and falling back to conversation.channel
conversation_updatedThe channel from body.channel must be Channel::Line
conversation_status_changedThe channel must be Channel::Line and the status must be resolved or open
contact_updatedAlways accepted; used to sync contact attributes

Every other event type is answered as skipped.

:::note Parity detail The channel check is written as "non-empty and not equal to Channel::Line", which means a request with no channel field at all passes, matching the original NestJS behaviour. message_type accepts both the string "outgoing" and the number 1, which JSON decoding produces as a float64. :::

  1. Once past the filter, the handler attaches _oaHash to the body.
  2. The entire body is published as bare JSON to the mbox_callback queue with a 5-second timeout, and the publish is genuinely awaited.
  3. On success it logs Mbox callback received at info level and responds 200 with {"status":"ok"}. On failure it logs an error and responds 500 with {"status":"error"}.
  4. If AMQP is not configured — for example when booting without backends — the publish is skipped but the response is still ok.

Every log line carries the event name and the conversation id, read from conversation.id and falling back to body.id.

Key Files & Functions

MethodRouteAuthHandler
POST/api/mbox/callback/:oaHashNonehandleCallback(deps)

The code lives in internal/mbox/handler.go. This package is a single file with no separate service or repository layer.

FunctionResponsibility
Register(r, deps)Mounts the route, deliberately outside the api-key guard
handleCallback(deps) gin.HandlerFuncRuns the entire flow above
accept(body map[string]any) boolThe five-event filter
isOutgoing(v any) boolAccepts "outgoing", float64(1), and json.Number("1")
nested, str, conversationID, logInfo, logErrorHelpers
The channelLine constantChatwoot's discriminator value, Channel::Line

Connections to Other Services

  • RabbitMQ — the mbox_callback queue, configured via RABBITMQ_QUEUE_MBOX_CALLBACK (default mbox_callback), sent as bare JSON on the line_exchange exchange.
  • worker-go — the consumer; it uses _oaHash to look up the OA, sends the agent's message back through the LINE Messaging API, and updates or clears the agent_mode key when a case is resolved.
  • Chatwoot — the calling system; the callback URL is configured on the Chatwoot side.
  • No database and no Redis are involved in this path.
  • The outbound direction, from LINE to Chatwoot, is covered in Handoff to Human Agents.
  • Risk worth noting — this endpoint is public, unauthenticated, and does not verify that the request genuinely came from Chatwoot. The only protections are a hard-to-guess oaHash and per-IP rate limiting.