KakaoTalk
TypeScript SDK reference for KakaoTalk — client, credential management, and types.
Installation
npm install agent-messengerimport { 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 disconnectEvents
| Event | Payload | Description |
|---|---|---|
message | KakaoTalkPushMessageEvent | New message received |
member_joined | KakaoTalkPushMemberEvent | Member joined a chat |
member_left | KakaoTalkPushMemberEvent | Member left a chat |
read | KakaoTalkPushReadEvent | Read receipt (unread count decreased) |
kakaotalk_event | KakaoTalkPushGenericEvent | Catch-all for all push events |
connected | { userId: string } | Connected to LOCO server |
disconnected | — | LOCO session dropped (see Reconnection) |
error | Error | Connection 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, emitsdisconnected, and immediately establishes a new session in the background. Subscribers receiveconnectedagain once it succeeds. No action required. - Network drop or peer close — The client emits
disconnectedand clears its session state. The next SDK API call (getChats,getMessages,sendMessage) will lazily re-establish the session viaexecuteWithReconnect. The listener itself stays passive — to resume push delivery after a drop, callawait client.acquireSession()(or any API call) from yourdisconnectedhandler. - Kicked by another device (
KICKOUT) — The listener stops and emitserror. This is terminal; you mustlogin()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()