koro-sdk
v0.2.0
Published
Official TypeScript SDK for the Koro API — end-to-end encrypted messaging, spaces, meetings, tasks, calendar and webhooks.
Maintainers
Readme
koro-sdk
Official TypeScript SDK for the Koro API — end-to-end encrypted messaging, spaces, meetings, tasks, calendar and webhooks.
Works in Node 18+ (ESM and CommonJS). Fully typed.
npm install koro-sdkQuickstart
import { Koro } from 'koro-sdk';
const koro = new Koro(); // reads KORO_TOKEN from the environment
// Who am I?
const me = await koro.me();
// Send an end-to-end encrypted message
await koro.messages.send('<conversationId>', 'Hello from a bot 👋');
// Create a space and schedule a meeting
const space = await koro.spaces.create({ name: 'My Team' });
const { url } = await koro.meetings.create({
title: 'Standup',
scheduled_at: '2026-07-01T09:00:00Z',
});
console.log('Join at', url);Authentication
Every request is authenticated with a Koro API key of the form
koro_live_<prefix>_<secret>, sent as Authorization: Bearer <token>.
The SDK reads the token from the KORO_TOKEN environment variable, or you can
pass it explicitly:
const koro = new Koro({ token: 'koro_live_abcd_…' });Create a .env in your project:
KORO_TOKEN=koro_live_abcd_yoursecret
# the bot device secret, shown once when you created the bot in the portal:
KORO_BOT_SECRET=base64secretkey…
# optional — defaults to https://api.koro.chat:3001
KORO_API_URL=https://api.koro.chat:3001The easiest way to get both values is to create a named bot in the Koro Developer Portal → Bots → Bot erstellen. The portal generates the bot's keypair in your browser and shows you
KORO_TOKENandKORO_BOT_SECRETonce — paste both into your.env. The bot then appears in chats with the name + avatar you chose.
If you have dotenv installed, the SDK auto-loads
.env for you. It is not a dependency — if it is absent the SDK simply
reads process.env directly. (Or call import 'dotenv/config' yourself.)
API keys are workspace-scoped. Scopes include messages:read|write,
conversations:read|write, tasks:read|write, users:read,
webhooks:manage. A missing scope returns HTTP 403 — see
Error handling.
End-to-end encryption (read this before sending messages)
Koro messages are end-to-end encrypted per recipient device using NaCl
box (Curve25519 + XSalsa20-Poly1305, via tweetnacl). The server only ever
stores opaque ciphertext — there is no plaintext path. A bot is just another
device: it owns a Curve25519 box keypair, and peers seal ciphertext to its
public key.
Bot identity (two ways)
The bot is a device with a Curve25519 box keypair. There are two ways to provide it, in order of preference:
1. Portal-issued KORO_BOT_SECRET (recommended). When you create a bot in
the Koro Developer Portal, its keypair is generated in your browser and the
secret is shown once. Set it in .env (or pass new Koro({ botSecret })). The
matching public key is already registered server-side, so the bot can send and
receive immediately — and shows up in chats with your chosen name + avatar.
const koro = new Koro(); // reads KORO_TOKEN + KORO_BOT_SECRET from .env2. Self-managed identity file (fallback). With no KORO_BOT_SECRET, the SDK
generates a keypair on first use and persists it to ./.koro/bot-identity.json
(override with new Koro({ identityFile })). You must then enroll the printed
public key on the server yourself.
const koro = new Koro();
console.log(koro.bot.identityPublicKey()); // base64 — enroll this on the serverEither way, the bot must be a member of the conversation to send/receive.
Add it via the portal (Bot → in Konversation einladen) or
POST /conversations/:id/members. Until the bot's public key is registered AND
it's a conversation member:
messages.send(...)is rejected (the bot device isn't a valid recipient), andmessages.list(...)returns envelopes withtext: null.
Keep
KORO_BOT_SECRET/./.koro/bot-identity.jsonprivate and out of version control — they contain the bot's secret key.
How messages.send works
1. GET /conversations/:id/devices → [{ device_id, identity_public_key }]
2. for each device: nacl.box(plaintext, nonce, devicePubKey, botSecretKey)
3. POST /messages { conversation_id, kind: 'text',
recipients: [{ device_id, ciphertext, nonce }] }Receiving messages
const messages = await koro.messages.list('<conversationId>', { limit: 50 });
for (const m of messages) {
if (m.text) console.log(m.sender_user_id, '→', m.text);
}list fetches the recipient copy addressed to the bot device and decrypts it
with nacl.box.open. Rows that aren't text, aren't addressed to the bot, or
fail to open come back with text: null but full envelope metadata.
Koro has no long-poll endpoint, so bots poll messages.list. See
examples/echo-bot.ts.
Message envelope format (assumption to reconcile)
The verified Koro mobile client seals the raw UTF-8 message string
directly — there is no JSON wrapper for kind: 'text'. The SDK matches this
byte-for-byte (EnvelopeFormat.Raw, the default), so messages it sends are
readable by the mobile app and vice-versa.
A structured { t: 'text', body } envelope is also implemented
(EnvelopeFormat.JsonTextBody) for forward-compatibility, and list will
transparently unwrap either form. If Koro later standardizes on the structured
envelope, switch with:
import { Koro, EnvelopeFormat } from 'koro-sdk';
const koro = new Koro({ envelopeFormat: EnvelopeFormat.JsonTextBody });API reference
Construct once and reuse:
const koro = new Koro({
token?: string, // default: process.env.KORO_TOKEN
apiUrl?: string, // default: process.env.KORO_API_URL ?? https://api.koro.chat:3001
identityFile?: string, // default: ./.koro/bot-identity.json
envelopeFormat?: EnvelopeFormat, // default: Raw
});koro.me()
GET /users/me → the identity behind the API key.
koro.messages
| Method | Description |
|---|---|
| send(conversationId, text, { reply_to_message_id? }) | E2E-encrypt + send a text message. Returns { message }. |
| list(conversationId, { limit?, before? }) | List + best-effort decrypt. Returns DecryptedMessage[]. |
| devices(conversationId) | The conversation's recipient devices. |
koro.conversations
| Method | Description |
|---|---|
| list() | Your conversations. |
| create({ kind, member_user_ids, title?, workspace_id? }) | Create a conversation. |
| devices(id) | Recipient devices of a conversation. |
koro.spaces (workspaces)
| Method | Description |
|---|---|
| list() | Spaces you belong to. |
| create({ name, description? }) | Create a space. |
| invite(spaceId, { role?, max_uses?, expires_at? }) | Create an invite code. |
| addChannel(spaceId, name) | Add a channel. |
koro.meetings
| Method | Description |
|---|---|
| create({ title, scheduled_at?, ... }) | Create a meeting. Returns { meeting, room_id, url }. |
| list() | Your meetings. |
koro.tasks
| Method | Description |
|---|---|
| list() | Your tasks. |
| create({ title, due_at?, priority?, workspace_id? }) | Create a task. |
| update(id, patch) | Patch a task. |
| complete(id) | Mark a task done. |
koro.calendar
| Method | Description |
|---|---|
| list() | Calendar events. |
| create({ title, starts_at, ends_at?, workspace_id? }) | Create an event. |
koro.webhooks (requires webhooks:manage)
| Method | Description |
|---|---|
| list() | Registered webhooks. |
| create({ workspace_id, url, events }) | Register a webhook. The returned secret is shown once. |
| delete(id) | Delete a webhook. |
koro.bot
| Method | Description |
|---|---|
| identityPublicKey() | Base64 public key of the bot device (enroll this). |
| keyPair() | The raw bot device keypair. |
koro.request(method, path, body?)
Low-level escape hatch for any endpoint not yet wrapped:
const calls = await koro.request('GET', '/calls');Error handling
Any non-2xx response throws a KoroError:
import { KoroError } from 'koro-sdk';
try {
await koro.messages.send(convId, 'hi');
} catch (err) {
if (err instanceof KoroError) {
console.error(err.status, err.message); // e.g. 403 "Missing scope: messages:write"
if (err.isScopeError) {
// 403 — your API key is missing a required scope
}
}
}KoroError carries status, message (from the API's { error } body),
body (raw parsed response), and request ({ method, path }).
Examples
examples/send-message.ts— send one message.examples/echo-bot.ts— poll a conversation and reply.examples/create-space.ts— space + channel + invite.
Run with tsx:
KORO_TOKEN=koro_live_… npx tsx examples/echo-bot.ts <conversationId>License
MIT
