Agent MessengerAgent Messenger
TypeScript SDK

KakaoTalk

TypeScript SDK reference for KakaoTalk — client, credential management, and types.

Installation

npm install agent-messenger
import { KakaoTalkClient, KakaoCredentialManager } from 'agent-messenger/kakaotalk'

KakaoTalkClient

The main client for interacting with KakaoTalk via the LOCO protocol. Unlike Discord/Slack which use stateless HTTP, KakaoTalk maintains a persistent TCP connection. Call close() when done.

import { KakaoTalkClient } from 'agent-messenger/kakaotalk'

const client = await new KakaoTalkClient().login({ oauthToken, userId, deviceUuid })
// deviceUuid is optional — defaults to `agent-messenger-${userId}`

Or use automatic credential extraction — credentials are read from stored login state:

import { KakaoTalkClient } from 'agent-messenger/kakaotalk'

const client = await new KakaoTalkClient().login()

Chat Rooms

// List recent chat rooms (from login snapshot)
const chats = await client.getChats()
// → KakaoChat[]

// List ALL chats (paginate beyond login snapshot)
const allChats = await client.getChats({ all: true })

// Search chats by display name
const results = await client.getChats({ search: 'Alice' })

// Resolve user-set room titles via CHATINFO (one extra LOCO call per chat).
// Each KakaoChat gets a `title: string | null` matching the in-app room name.
// For open chats with no user-set title, the SDK falls back to the OpenLink
// link name via an additional INFOLINK call (only for OM / OD chats).
const titled = await client.getChats({ resolveTitles: true })

// Resolve a single chat's title directly (returns null on error or missing title)
const title = await client.getChatTitle('9876543210')

Messages

// Get recent messages from a chat (default: 20)
const messages = await client.getMessages('9876543210')
// → KakaoMessage[]

// Get more messages
const more = await client.getMessages('9876543210', { count: 100 })

// Get messages after a known log ID
const newer = await client.getMessages('9876543210', { from: '123456789' })

Each KakaoMessage carries an author_name: string | null. The SDK auto-populates this from the same chat-list response that already arrives at login (no extra LOCO calls). It resolves for "display members" — the small set of nicknames KakaoTalk includes in the chat list — and is null for everyone else.

Each KakaoMessage also carries an attachment: Record<string, unknown> | null. For text messages this is null. For non-text messages (photos, videos, files, stickers, etc.) it is the parsed LOCO attachment payload — a JSON object whose shape varies by type. Photo messages use fields like k (CDN key), w/h (dimensions), mt (mime type), and url; sticker messages use path/emoticonItemPath. Treat it as opaque and narrow per type.

Members

// List all members of a chat (LOCO GETMEM)
const members = await client.getMembers('9876543210')
// → KakaoMember[]

// Resolve a specific subset by user_id (LOCO MEMBER) — useful for >100-member rooms
const subset = await client.getMembersByIds('9876543210', ['1234567890', '9876543210'])

Each KakaoMember includes user_id, nickname, profile image URLs, status_message, country_iso, user_type (nullable; null when the server omits the field), and open-chat-only fields (open_token, open_profile_link_id, open_permission — the last is the OpenChannelUserPerm bitfield: 1=OWNER, 2=NONE, 4=MANAGER, 8=BOT).

Sending

// Send a text message
const result = await client.sendMessage('9876543210', 'Hello from SDK!')
// → KakaoSendResult { success, status_code, chat_id, log_id, sent_at }

Attachments

sendAttachment is the single entry point for outbound photos, videos, audio, files, and multi-photo galleries. It accepts either one buffer or an array of { data, filename, mime? } tuples; MIME is sniffed from the filename when not given, and the client dispatches to the right LOCO message_type internally.

// Single attachment — MIME inferred from the filename extension
//   image/* → photo, video/* → video, audio/* → audio, anything else → file
await client.sendAttachment('9876543210', photoBytes, 'cat.jpg')
await client.sendAttachment('9876543210', pdfBytes, 'report.pdf')

// Force a specific MIME (extension-less files, or to send a video as a generic file)
await client.sendAttachment('9876543210', mp4Bytes, 'clip.mp4', 'application/octet-stream')

// Multiple attachments — the client picks the right wire flow:
//   all-image (length >= 2) → multi-photo gallery (sendMultiPhoto under the hood)
//   mixed kinds              → sequential individual sends, fail-fast on first error
//   length === 1             → equivalent to the single-buffer overload
await client.sendAttachment('9876543210', [
  { data: photo1, filename: 'a.jpg' },
  { data: photo2, filename: 'b.png' },
  { data: photo3, filename: 'c.webp' },
])

await client.sendAttachment('9876543210', [
  { data: photo, filename: 'screenshot.png' },
  { data: spec, filename: 'spec.pdf' },
])
// → KakaoSendResult of the last successful send (or the first failure, if any)

The typed helpers sendPhoto, sendVideo, sendAudio, sendFile, and sendMultiPhoto remain available when you want to bypass MIME sniffing and dispatch to a specific message_type explicitly.

Cleanup

// Close the LOCO connection (MUST call when done)
client.close()

Once closed, any subsequent method call throws a KakaoTalkError with code client_closed. Create a new client if you need to reconnect.

KakaoTalkListener

Real-time event listener that receives push events from KakaoTalk via the LOCO protocol. Subscribes to the underlying client's LOCO session.

import { KakaoTalkClient, KakaoTalkListener } from 'agent-messenger/kakaotalk'

const client = await new KakaoTalkClient().login()
const listener = new KakaoTalkListener(client)

listener.on('connected', (info) => {
  console.log(`Connected as ${info.userId}`)
})

listener.on('message', (event) => {
  console.log(`[${event.chat_id}] ${event.author_id}: ${event.message}`)
})

listener.on('member_joined', (event) => {
  console.log(`User ${event.member.user_id} joined ${event.chat_id}`)
})

listener.on('member_left', (event) => {
  console.log(`User ${event.member.user_id} left ${event.chat_id}`)
})

listener.on('read', (event) => {
  console.log(`User ${event.user_id} read ${event.chat_id} up to ${event.watermark}`)
})

listener.on('disconnected', () => console.log('LOCO session dropped'))
listener.on('error', (err) => console.error(err))

await listener.start()
// listener.stop() to disconnect

Events

EventPayloadDescription
messageKakaoTalkPushMessageEventNew message received
member_joinedKakaoTalkPushMemberEventMember joined a chat
member_leftKakaoTalkPushMemberEventMember left a chat
readKakaoTalkPushReadEventRead receipt (unread count decreased)
kakaotalk_eventKakaoTalkPushGenericEventCatch-all for all push events
connected{ userId: string }Connected to LOCO server
disconnectedLOCO session dropped (see Reconnection)
errorErrorConnection or protocol error

Reconnection

The listener does not implement a reconnection loop. Behavior on session drop:

  • Server-requested migration (CHANGESVR) — The client invalidates the session, emits disconnected, and immediately establishes a new session in the background. Subscribers receive connected again once it succeeds. No action required.
  • Network drop or peer close — The client emits disconnected and clears its session state. The next SDK API call (getChats, getMessages, sendMessage) will lazily re-establish the session via executeWithReconnect. The listener itself stays passive — to resume push delivery after a drop, call await client.acquireSession() (or any API call) from your disconnected handler.
  • Kicked by another device (KICKOUT) — The listener stops and emits error. This is terminal; you must login() a new client to recover.

If you need a long-running listener that survives network drops, wrap acquireSession() in your own retry loop driven by the disconnected event.

KakaoCredentialManager

Manages KakaoTalk credentials stored at ~/.config/agent-messenger/kakaotalk-credentials.json. Files are written with 0o600 permissions.

import { KakaoCredentialManager } from 'agent-messenger/kakaotalk'

const manager = new KakaoCredentialManager()
// Load full config from disk (returns defaults if file doesn't exist)
const config = await manager.load()
// → KakaoConfig { current_account, accounts }

// Save full config to disk
await manager.save(config)

// Get the current account's credentials
const account = await manager.getAccount()
// → KakaoAccountCredentials | null

// Get a specific account by ID
const specific = await manager.getAccount('1234567890')
// → KakaoAccountCredentials | null

// Store account credentials
await manager.setAccount(credentials)

// Remove an account
await manager.removeAccount('1234567890')

// Switch the current account
await manager.setCurrentAccount('1234567890')

Types

import type {
  KakaoChat,
  KakaoMember,
  KakaoMessage,
  KakaoSendResult,
  KakaoAccountCredentials,
  KakaoConfig,
  KakaoDeviceType,
  PendingLoginState,
} from 'agent-messenger/kakaotalk'

Zod Schemas

Runtime-validated schemas are also exported for parsing API responses:

import {
  KakaoChatSchema,
  KakaoMemberSchema,
  KakaoMessageSchema,
  KakaoSendResultSchema,
  KakaoAccountCredentialsSchema,
  KakaoConfigSchema,
} from 'agent-messenger/kakaotalk'

Examples

Chat Summary

List all chats, count unread, and print a summary.

import { KakaoTalkClient, KakaoCredentialManager } from 'agent-messenger/kakaotalk'

const manager = new KakaoCredentialManager()
const account = await manager.getAccount()
if (!account) throw new Error('Not authenticated')

const client = await new KakaoTalkClient().login({
  oauthToken: account.oauth_token,
  userId: account.user_id,
  deviceUuid: account.device_uuid,
})

try {
  const chats = await client.getChats({ all: true })
  const unread = chats.filter((c) => c.unread_count > 0)
  console.log(`${unread.length} chats with unread messages:`)
  for (const chat of unread) {
    console.log(`  ${chat.display_name} — ${chat.unread_count} unread`)
  }
} finally {
  client.close()
}

Send Notification

Send a message to a specific chat.

import { KakaoTalkClient, KakaoCredentialManager } from 'agent-messenger/kakaotalk'

const manager = new KakaoCredentialManager()
const account = await manager.getAccount()
if (!account) throw new Error('Not authenticated')

const client = await new KakaoTalkClient().login({
  oauthToken: account.oauth_token,
  userId: account.user_id,
  deviceUuid: account.device_uuid,
})

try {
  const chats = await client.getChats({ search: 'Team' })
  const teamChat = chats[0]

  if (teamChat) {
    const result = await client.sendMessage(teamChat.chat_id, '🚀 Deployment complete!')
    console.log(`Sent (log_id: ${result.log_id})`)
  }
} finally {
  client.close()
}

Message Monitor

Poll for new messages in a chat room.

import { KakaoTalkClient, KakaoCredentialManager } from 'agent-messenger/kakaotalk'

const manager = new KakaoCredentialManager()
const account = await manager.getAccount()
if (!account) throw new Error('Not authenticated')

const client = await new KakaoTalkClient().login({
  oauthToken: account.oauth_token,
  userId: account.user_id,
  deviceUuid: account.device_uuid,
})
const chatId = '9876543210'

let lastLogId: string | undefined

const poll = async () => {
  try {
    const messages = lastLogId
      ? await client.getMessages(chatId, { from: lastLogId })
      : await client.getMessages(chatId, { count: 1 })

    for (const msg of messages) {
      if (msg.log_id !== lastLogId) {
        console.log(`[${msg.author_id}] ${msg.message}`)
        lastLogId = msg.log_id
      }
    }
  } catch (error) {
    console.error('Poll failed:', error)
  }
}

const timer = setInterval(poll, 10_000)

// To stop: clearInterval(timer); client.close()

On this page