Complete reference documentation for the HyperBabel API Platform.
All API requests must include your API Key in the Authorization header. Generate your API key from Console → API Keys.
Authorization: Bearer hb_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxAPI Key Types
hb_live_... — Production key (billing applies)https://api.hyperbabel.com/api/v1Monitor your API usage programmatically. Use this for billing alert automation or to build internal cost dashboards. Account management (register, login, API key creation) is done through the HyperBabel Console, not via API.
/api/v1/auth/usageReturn the current month's API usage breakdown by service. Use for programmatic monitoring or billing alert automation.
// GET — no body // Headers Authorization: Bearer hb_live_xxx
{
"period": "2026-03",
"plan": "pro",
"usage": {
"chat_messages": 12400,
"video_minutes": 320,
"stream_minutes": 60,
"translations": 8900,
"storage_used_mb": 1240
}
}Mint short-lived JWTs scoped to one of your end-users so your mobile and web apps never need to embed an HyperBabel API key. Your backend stays the only place that holds hb_live_…; client apps carry only per-user tokens.
[ Client App ] ──Sign in with Firebase──→ Firebase ID token
│
│ POST /customer/auth/firebase-exchange (Firebase users — direct, no backend hop)
│ Authorization: Bearer <firebase-id-token>
▼
[ HyperBabel API ] ── verify token → mint customer JWT
│
│ { access_token, refresh_token, ... }
▼
[ Client App ] ──Bearer <customer access token>──→ /chat, /video, /translate
Other auth providers (Auth0 / Cognito / your own): your backend calls
/customer/issue-token with the org API key and forwards the JWT to the client.⚠️ Why this exists
Embedding hb_live_… in a mobile app means anyone who decompiles the APK / IPA gets full access to your org. Web apps face the same risk through the network tab. This session-token-based Customer Auth pattern is the recommended approach for API key security in client apps — short-lived per-user tokens keep your org credential on your backend where it belongs.
When to use which credential
hb_live_…
Org API key
Server-to-server only — your backend, Cloud Function, Lambda. Never ship in a mobile or browser binary.
eyJ… (customer JWT)
Per-end-user, 1h access
Every SDK-facing call from a client app — chat, video, translate, RTM, presence, push, storage. Mint via /customer/auth/firebase-exchange (Firebase users, client-direct) or /customer/issue-token (other auth providers, server-to-server).
/api/v1/customer/auth/firebase-exchangeFor apps already using Firebase Auth. The client sends its short-lived Firebase ID token in the Authorization header — HyperBabel verifies it against Google's JWKS and mints a customer JWT for the same end-user. Register your Firebase project ID in the Console (Dashboard → Customer Auth → Firebase Projects) before calling. external_user_id, display_name, and email are auto-extracted from the verified token.
POST /api/v1/customer/auth/firebase-exchange Authorization: BearerContent-Type: application/json { "preferred_lang_cd": "ko-KR" // optional: send the device locale; the // server normalises against the supported // translation languages and falls back to 'en' }
// Same shape as /customer/issue-token { "access_token": "eyJhbGc…", "refresh_token": "eyJhbGc…", "expires_at": 1762450000, "refresh_expires_at": 1764991600, "user_id": "079632a3-…", "external_user_id": "firebase-uid", // Firebase UID "org_id": "11b20b79-…", "session_id": "02ddc384-…", "preferred_lang_cd": "ko", // canonical code actually stored "token_type": "Bearer" }
/api/v1/customer/issue-tokenMint a (access_token, refresh_token) pair scoped to one of your end-users. Server-to-server only — never call from a mobile or browser app. external_user_id is your stable opaque identifier (UUID recommended).
preferred_lang_cd is used to seed the user's preferred language on their first token issuance only. On subsequent calls for an existing user, the value stored in HyperBabel is preserved — sending a different code does not overwrite the user's chosen language. To update it explicitly, use PATCH /api/v1/customer/users/me from the client app.
POST /api/v1/customer/issue-token
Authorization: Bearer hb_live_xxx
Content-Type: application/json
{
"external_user_id": "alice-uuid",
"display_name": "Alice Chen",
"preferred_lang_cd": "en"
}{
"access_token": "eyJhbGc…",
"refresh_token": "eyJhbGc…",
"expires_at": 1762450000,
"refresh_expires_at": 1764991600,
"user_id": "079632a3-…",
"external_user_id": "alice-uuid",
"org_id": "11b20b79-…",
"session_id": "02ddc384-…",
"preferred_lang_cd": "en", // canonical code actually stored
"token_type": "Bearer"
}/api/v1/customer/refreshRotate a JWT pair using its refresh token. Safe to call directly from the client app — the refresh token in the body authenticates the request, no Authorization header needed. Old refresh token is revoked atomically; new pair has a fresh session_id.
POST /api/v1/customer/refresh
Content-Type: application/json
{
"refresh_token": "eyJhbGc…"
}// Same shape as /customer/issue-token { "access_token": "eyJhbGc…", // new "refresh_token": "eyJhbGc…", // rotated "session_id": "1b124c26-…", // new sid ... }
/api/v1/customer/revokeInvalidate every active session for one external_user_id. Global propagation under 60 seconds. Use on log-out-everywhere, password-changed, or abuse signals.
POST /api/v1/customer/revoke
Authorization: Bearer hb_live_xxx
Content-Type: application/json
{
"external_user_id": "alice-uuid"
}{
"revoked": 3
}/api/v1/customer/meVerifies the customer JWT and returns the identity it represents. Useful for client-side debugging and 'who am I' UX.
GET /api/v1/customer/me Authorization: Bearer
{
"org_id": "11b20b79-…",
"user_id": "079632a3-…",
"external_user_id": "alice-uuid",
"display_name": "Alice Chen",
"preferred_lang_cd": "en",
"session_id": "02ddc384-…"
}Token lifetimes & safety boundary
/refresh./chat, /unitedchat, /video, /translate, /rtm, /presence, /push, /storage, /users — exactly as the org API key. They cannot reach /admin, /billing, or /auth/api-keys./revoke propagates within ~60 seconds globally via edge KV./refresh rotates the pair atomically (the previous refresh token is revoked) and logs the current device's user-agent and IP, viewable in Console → Customer Auth → End Users.hb_live_ or hb_test_ — catches accidental regressions where a future commit reintroduces the org key.Both open-source sample demos wire the full Firebase Direct Exchange flow end-to-end — sign-in, customer JWT issuance, silent refresh, and the hb_live_/hb_test_ guard described above. Pick the platform you ship on.
HyperBabel provides two complementary chat APIs. Both share the same underlying infrastructure — choose based on how much you want HyperBabel to manage for you.
/api/v1/chat/Pure messaging layer. You manage rooms & members. Best for embedding messaging into an app that already has its own user/room management.
Includes
/api/v1/unitedchat/Complete chat solution. HyperBabel manages rooms, roles & video. Best for building a full-featured chat app quickly.
Everything in Chat API, plus
🔍 Pick by scenario
Build a KakaoTalk / LINE-style chat app
🚀United Chat APIAdd 1:1 live support / CS chatbot to my existing app
💬Chat APICommunity / fan club with open rooms and moderators
🚀United Chat APIIoT device log stream or server-to-server messaging
💬Chat APIGroup video call + chat room integration
🚀United Chat APII already manage my own rooms/users — just need messaging
💬Chat APIUnified room-based chat with built-in 1:1, Group, and Open chat support — including real-time messaging, role management, and integrated video calls. All endpoints require Authorization: Bearer hb_live_xxx and the chat scope.
/api/v1/unitedchat/roomsCreate a chat room. room_type: '1to1' | 'group' | 'open'. For 1:1, pass members with 1 user_id. For group, pass members array. Returns already_exists:true if a 1:1 between those users already exists.
// 1:1 room { "room_type": "1to1", "creator_id": "user_A", "members": ["user_B"] } // Group room { "room_type": "group", "room_name": "Project Alpha", "creator_id": "user_A", "members": ["user_B", "user_C"] } // Open room (public, up to 500 members) { "room_type": "open", "room_name": "Community Hub", "creator_id": "user_A" }
{
"room": {
"id": "uuid-xxxx",
"channel_type": "group",
"channel_name": "uc-m8z3k1",
"room_type": "group",
"realtime_channel": "hb:org_uuid:uc-m8z3k1",
"created_at": "2026-03-11T10:00:00Z"
}
}/api/v1/unitedchat/roomsGet all rooms the user is a member of, plus joinable open rooms. Pass user_id as a query param. Pass lang (e.g. 'ko', 'ja') to enable automatic description translation — translations are cached in the DB on first request so subsequent calls cost nothing extra. The response includes description_translated alongside the original description when a translation is available. Each entry in rooms also embeds last_message — the most recent message in that channel (or null if empty) — so a chat-list UI can render previews without a follow-up call per room. Deleted messages are returned with deleted_at set so the client can render a 'Message deleted' placeholder.
// GET — no body // Query: ?user_id=user_A&lang=ko
{
"rooms": [
{
"id": "uuid-xxxx",
"channel_type": "group",
"room_name": "Project Alpha",
"realtime_channel": "hb:org_uuid:uc-m8z3k1",
"member_count": "3",
"unread_count": "5",
"last_message": { // null when the room has no messages yet
"id": "msg-uuid-zzzz",
"channel_id": "uuid-xxxx",
"sender_id": "user_B",
"content": "안녕하세요",
"message_type": "text", // text | image | video | audio | file | location | contact | video_call | system
"sender_lang_cd": "ko", // ISO-639-1 the sender wrote in
"translated_content": { "en": "Hello" }, // server-side cache; populated lazily as users request translations
"metadata": null,
"deleted_at": null, // ISO timestamp when the sender removed the message
"created_at": "2026-05-20T12:34:56Z",
"updated_at": null
}
}
],
"open_rooms": [
{
"id": "uuid-yyyy",
"display_name": "Community Hub",
"description": "A space for HyperBabel developers 🚀",
"description_translated": "HyperBabel 개발자를 위한 공간 🚀", // present only when lang param is set & translation succeeds
"invite_code": "a1b2c3d4",
"member_count": "42",
"is_joinable": true
}
]
}/api/v1/unitedchat/rooms/:roomIdGet room details including full member list with roles.
// GET — no body // Path: :roomId = room UUID
{
"room": {
"id": "uuid-xxxx",
"channel_type": "group",
"display_name": "Project Alpha",
"realtime_channel": "hb:org_uuid:uc-m8z3k1",
"members": [
{ "user_id": "user_A", "role": "owner", "joined_at": "2026-03-11T10:00:00Z" },
{ "user_id": "user_B", "role": "member", "joined_at": "2026-03-11T10:01:00Z" }
]
}
}/api/v1/unitedchat/rooms/:roomId/nameSet a custom display name for the room (per-user). Each member can have their own name for the room.
{
"user_id": "user_A",
"custom_name": "My Alpha Team"
}{ "success": true }/api/v1/unitedchat/rooms/:roomId/descriptionSet or update the description for an open room (owner only). The description is visible in the open room list, helping users understand the room's purpose before joining. Pass null to clear.
{
"user_id": "owner_user",
"description": "A public room for discussing HyperBabel integrations 🚀"
}{ "success": true, "description": "A public room for discussing HyperBabel integrations 🚀" }/api/v1/unitedchat/rooms/join-by-codeJoin an open room using its 8-character invite code — no room ID required. How to get the code: open the chat room → click 🛡️ Members in the header → copy the Invite Code, or click 📋 Copy Link which generates a shareable URL like /join?code=a1b2c3d4 — extract the code param. ⚠️ user_id does NOT need to be pre-registered in HyperBabel — pass your own app's user identifier directly. Idempotent (safe to call multiple times). Banned users get 403. Full rooms get 403 room_full. On first join, broadcasts 'Yujin joined the room' system message to all members in real time.
// ── How to get the invite_code (programmatic) // Option A — List all open rooms the user hasn't joined yet: // GET /api/v1/unitedchat/rooms?user_id=X // → response.open_rooms[N].invite_code // // Option B — Get a specific room's details (member or owner): // GET /api/v1/unitedchat/rooms/:roomId // → response.room.invite_code // // Option C — UI (Console or your app): // Chat header → 🛡️ Members → Invite Code box → 📋 Copy Link // Shareable URL format: https://yourconsole.com/join?code=XXXXXXXX // ── Step 1: Authorization header (required for every request) Authorization: Bearer hb_live_xxxxxxxxxxxxxxxxxxxx // ── Step 2: Request body { // 8-char invite code obtained via API or Copy Link above "invite_code": "a1b2c3d4", // Your own app's user ID — no pre-registration in HyperBabel needed // Pass any string that uniquely identifies the user in your system "user_id": "user_C", // Optional: shown in the system message broadcast to the room // e.g. "Yujin joined the room" // Falls back to user_id if omitted "display_name": "Yujin" } // ── Error cases // 400 — invite_code or user_id missing // 403 — user is banned from this room (code: "banned") // 403 — room has reached its member limit (code: "room_full") // 404 — invalid invite code or room not found
{
"success": true,
// Use room_id for all subsequent message / member API calls
"room_id": "uuid-room-zzz",
"room_name": "HyperBabel Dev Chat",
// Subscribe to this HyperBabel Realtime channel to receive real-time messages
"realtime_channel": "hb:org_uuid:uc-m8z3k1"
}/api/v1/unitedchat/rooms/:roomId/joinJoin an open (public) room by room ID. Banned users cannot rejoin. Idempotent — safe to call on re-entry. Broadcasts a system message 'X joined the room' to all current members on a genuine first join, plus a structured `room.member_changed` realtime event with action='joined' so SDK clients can patch their member list without re-fetching.
{ "user_id": "user_C", "display_name": "Yujin" }{ "success": true }/api/v1/unitedchat/rooms/:roomId/leaveRemove a user from a room. Removes membership and custom name. For open rooms, the user can rejoin later using the room ID or invite code. Broadcasts a system message 'X left the room' to all remaining members, plus a structured `room.member_changed` realtime event with action='left'.
{ "user_id": "user_B", "display_name": "Bob" }{ "success": true }/api/v1/unitedchat/policyReturns the org's file transfer policy for open rooms. If no custom policy exists yet, platform defaults are returned. Use this in your app to enforce client-side upload rules.
// GET — no body (Bearer token required){
"policy": {
"files_enabled": true, // true = uploads allowed, false = all uploads blocked
"max_file_size_mb": 10, // per-file size cap (integer, 1–100)
"allowed_types": ["image/*"], // MIME wildcards or extensions (e.g. ".pdf", "video/*")
"updated_at": null // null if using platform defaults
}
}/api/v1/unitedchat/policyUPSERT the org's open room file transfer policy. All fields are optional — omit a field to keep the current value. Changes take effect immediately for any client that fetches the policy.
{
"files_enabled": true,
"max_file_size_mb": 25, // 1–100 MB
"allowed_types": ["image/*", "video/*", ".pdf"]
}{
"success": true,
"policy": {
"files_enabled": true,
"max_file_size_mb": 25,
"allowed_types": ["image/*", "video/*", ".pdf"],
"updated_at": "2026-03-19T15:00:00Z"
}
}/api/v1/unitedchat/rooms/:roomId/readMark all messages in the room as read for a user. Resets the unread_count to 0 for that user. Also broadcasts a real-time read_receipt event to the room channel so the sender's client can instantly update the message status icon from ✓ (sent) to ✓✓ (read).
{ "user_id": "user_A" }{ "success": true }
// Real-time event broadcast to all room members:
{
"type": "read_receipt",
"data": {
"user_id": "user_A",
"last_read_message_id": "uuid-msg-yyy",
"read_at": "2026-03-18T22:00:00.000Z"
}
}/api/v1/unitedchat/rooms/:roomId/messagesSend a message to a room. message_type: 'text' | 'image' | 'video' | 'audio' | 'file' | 'location' | 'contact'. Supports reply threading via reply_to. Returns 403 with code 'blocked_by_recipient' if a recipient has blocked the sender — handle this by informing the user they cannot send messages to this room.
// ── Text message { "sender_id": "user_A", "content": "Hello!", "message_type": "text", "reply_to": "uuid-msg-xxx" // optional — thread reply } // ── Location message { "sender_id": "user_A", "content": "📍 Seoul City Hall", "message_type": "location", "metadata": { "name": "Seoul City Hall", "address": "Jung-gu, Seoul", "latitude": 37.5665, "longitude": 126.9780 } } // ── Contact message { "sender_id": "user_A", "content": "👤 Jane Doe", "message_type": "contact", "metadata": { "name": "Jane Doe", "phone": "+82-10-1234-5678", "email": "jane@example.com" } }
{
"message": {
"id": "uuid-msg-yyy",
"channel_id": "uuid-xxxx",
"sender_id": "user_A",
"content": "📍 Seoul City Hall",
"message_type": "location",
"metadata": { "name": "Seoul City Hall", "latitude": 37.5665, "longitude": 126.9780 },
"created_at": "2026-03-18T10:05:00Z"
}
}/api/v1/unitedchat/rooms/:roomId/messagesGet messages for a room with cursor-based pagination. Pass before_id for older messages. The response also includes read_statuses for all members — use these to render delivery/read icons (✓ sent, ✓✓ read, N unread count for group rooms).
// GET — no body // Query: ?before_id=uuid-msg-yyy&limit=50
{
"messages": [
{
"id": "uuid-msg-yyy",
"sender_id": "user_A",
"content": "Hello everyone!",
"message_type": "text",
"created_at": "2026-03-11T10:05:00Z"
}
],
"has_more": true,
"next_cursor": "uuid-msg-aaa",
"read_statuses": [
{
"user_id": "user_B",
"last_read_message_id": "uuid-msg-yyy",
"last_read_at": "2026-03-11T10:06:00Z"
}
]
}/api/v1/unitedchat/rooms/:roomId/banBan a user from a room. Only room owner or sub_admin can ban. Banned users are removed from membership and cannot rejoin. Publishes a `room.member_changed` realtime event with action='banned' so the banned user's client can navigate them out and other members can update their member list instantly.
{
"user_id": "user_B",
"banned_by": "user_A"
}{ "success": true, "banned_user": "user_B", "banned_by": "user_A" }/api/v1/unitedchat/rooms/:roomId/ban/:userIdLift a ban for a user. Only owner or sub_admin can unban. Publishes a `room.member_changed` realtime event with action='unbanned'. Note: the unbanned user must call POST /join to rejoin — unban only removes the bar.
{ "unbanned_by": "user_A" }{ "success": true, "unbanned_user": "user_B" }/api/v1/unitedchat/rooms/:roomId/bansList all ban records for a room, most recent first. Includes both active bans and lifted (unbanned) records — check is_active to distinguish. Useful for rendering a moderation history or a 'banned users' admin panel.
// GET — no body{
"bans": [
{
"user_id": "user_B",
"banned_by": "user_A",
"banned_at": "2026-03-11T10:05:00Z",
"unbanned_by": null,
"unbanned_at": null,
"is_active": true
}
]
}/api/v1/unitedchat/rooms/:roomId/sub-adminsPromote a member to sub_admin role. Only the room owner can promote. Maximum 10 sub_admins per room. Publishes a `room.member_changed` realtime event with action='promoted' and new_role='sub_admin' so client UIs can refresh role badges live.
{
"user_id": "user_B",
"owner_id": "user_A"
}{ "success": true, "sub_admin": "user_B" }/api/v1/unitedchat/rooms/:roomId/sub-admins/:userIdDemote a sub_admin back to member. Only the room owner can demote (owner_id is required and validated). Publishes a `room.member_changed` realtime event with action='demoted' and new_role='member' so client UIs refresh role badges live. No-op if the target is not currently a sub_admin.
{ "owner_id": "user_A" }{ "success": true }/api/v1/unitedchat/rooms/:roomId/membersGet paginated member list for a room, ordered by role (owner → sub_admin → member). Supports page/limit pagination and partial name search.
// GET — no body // Query params (all optional): ?page=1 // Page number, default: 1 ?limit=50 // Members per page, default: 50, max: 100 ?search=alice // Filter by user name (partial, case-insensitive)
{
"members": [
{ "user_id": "user_A", "role": "owner", "joined_at": "2026-03-11T10:00:00Z" },
{ "user_id": "user_B", "role": "sub_admin", "joined_at": "2026-03-11T10:01:00Z" },
{ "user_id": "user_C", "role": "member", "joined_at": "2026-03-11T10:02:00Z" }
],
"total": 142,
"page": 1,
"limit": 50,
"total_pages": 3
}/api/v1/unitedchat/rooms/:roomId/messages/:messageIdSoft-delete a message. Sets deleted_at — the message is excluded from getMessages responses. Only the original sender, room owner, or sub_admin can delete.
{ "user_id": "user_A" }{ "success": true, "message_id": "uuid-msg-xxx", "deleted_at": "2026-03-11T15:00:00Z" }/api/v1/unitedchat/rooms/:roomId/messages/:messageIdEdit a text message. Only the original sender can edit. Only text type messages are editable. The original content is preserved in original_content for audit trail. edited_at is set automatically.
{
"user_id": "user_A",
"content": "Updated message content"
}{
"success": true,
"message": {
"id": "uuid-msg-xxx",
"content": "Updated message content",
"original_content": "Original message content",
"edited_at": "2026-03-11T15:01:00Z"
}
}/api/v1/unitedchat/rooms/:roomId/muteMute push notifications for a room. Omit duration_minutes for indefinite mute.
{
"user_id": "user_A",
"duration_minutes": 60 // optional — omit for indefinite
}{
"success": true,
"channel_id": "uuid-xxxx",
"muted_until": "2026-03-11T16:00:00Z",
"is_indefinite": false
}/api/v1/unitedchat/rooms/:roomId/muteRemove mute from a room for a user.
{ "user_id": "user_A" }{ "success": true, "channel_id": "uuid-xxxx", "user_id": "user_A" }/api/v1/unitedchat/rooms/:roomId/mute/:userIdCheck whether a user has muted a room and until when.
// GET — no body{
"is_muted": true,
"channel_id": "uuid-xxxx",
"user_id": "user_A",
"muted_until": "2026-03-11T16:00:00Z",
"is_indefinite": false
}/api/v1/unitedchat/rooms/:roomId/freezeFreeze a room so that only the owner and sub_admins can send messages. Only owner or sub_admin can freeze. Publishes a `room.updated` realtime event so all connected clients flip the room into frozen mode (input becomes read-only) without a re-fetch.
{ "user_id": "user_A" }{ "success": true, "is_frozen": true, "channel_id": "uuid-xxxx" }/api/v1/unitedchat/rooms/:roomId/freezeUnfreeze a room to restore normal messaging for all members. Publishes a `room.updated` realtime event so connected clients re-enable the message input immediately.
{ "user_id": "user_A" }{ "success": true, "is_frozen": false, "channel_id": "uuid-xxxx" }/api/v1/unitedchat/rooms/:roomId/pin/:messageIdPin a message to the top of the room. Only 1 pinned message per room at a time — pinning a new message automatically replaces the existing pin. Supports all message types (text, image, file, video, audio, location, contact) except system and video_call messages. Permission: 1:1 rooms — any member; group / open rooms — owner or sub_admin only.
{ "user_id": "user_A" }{ "success": true, "pinned_message_id": "msg-uuid-xxxx" }/api/v1/unitedchat/rooms/:roomId/pinRemove the currently pinned message from the room. Permission: 1:1 rooms — any member; group / open rooms — owner or sub_admin only.
{ "user_id": "user_A" }{ "success": true }/api/v1/unitedchat/rooms/:roomId/searchSearch messages in a room using high-performance full-text search. Always provide created_at_gte + created_at_lte to enable indexing optimizations — max range 6 months, defaults to last 6 months. Results include a highlight field with matched terms wrapped in <mark> tags. Query syntax: spaces = AND, quotes = phrase match.
// Query params ?q=hello+world // Required — search query (spaces=AND, quotes=phrase) ?limit=20 // Optional, max 50 ?created_at_gte=2026-03-01T00:00:00Z ?created_at_lte=2026-03-11T23:59:59Z // Response header X-Applied-Created-At-Gte:
{
"messages": [
{
"id": "msg_456",
"sender_id": "user_123",
"sender_name": "Alice",
"content": "Hello World!",
"highlight": "Hello World!",
"created_at": "2026-03-01T10:01:00Z"
}
],
"query": "hello world",
"applied_range": {
"gte": "2025-09-11T23:37:00Z",
"lte": "2026-03-11T23:37:00Z"
}
}/api/v1/unitedchat/rooms/:roomId/typingBroadcast a typing event to eligible room members via the real-time channel. HyperBabel applies per-user send/receive preferences (configurable via the Typing Prefs API) and only publishes when at least one member has receive enabled. Supported for: 1:1 rooms, and group rooms with ≤ 4 members. Self-chat rooms are automatically skipped. Recommended: call when the user starts typing, throttled to once every 2 seconds. Recipients should auto-clear the indicator after 3 seconds of no new events.
{
"user_id": "user_A",
"display_name": "Alice" // shown as "Alice is typing..."
}// Published: HyperBabel real-time channel broadcast { "success": true, "user_id": "user_A" } // Skipped (non-error): send/recv prefs not met, group > 4 members, or self-chat { "success": true, "skipped": true, "reason": "send_disabled" | "no_recv_enabled" | "group_too_large" } // Real-time event payload received by eligible room members: { "type": "typing", "userId": "user_A", "userName": "Alice" }
/api/v1/unitedchat/rooms/:roomId/typing-prefsReturns each member's per-room typing indicator preferences (send and receive). If a member has not yet configured their preferences, both fields default to true (fail-open policy — typing indicators are ON by default unless explicitly disabled).
// No body required{
"prefs": [
{ "user_id": "user_A", "send_enabled": true, "recv_enabled": false, "updated_at": "..." },
{ "user_id": "user_B", "send_enabled": false, "recv_enabled": true, "updated_at": "..." }
]
}/api/v1/unitedchat/rooms/:roomId/typing-prefsSave or update the calling user's per-room typing indicator preferences. Uses UPSERT semantics — safe to call on every room entry. send_enabled controls whether this user's typing events are published. recv_enabled controls whether this user receives others' typing events. Defaults are true (fail-open) — both indicators are ON unless the user explicitly disables them.
{
"user_id": "user_A",
"send_enabled": true, // default: true (fail-open)
"recv_enabled": true // default: true (fail-open)
}{ "success": true, "send_enabled": true, "recv_enabled": true }Real-time messaging with channel management, cursor-based pagination, reactions, read receipts, and built-in AI translation. All endpoints require Authorization: Bearer hb_live_xxx and the chat scope.
/api/v1/chat/channelsCreate a new chat channel. channel_type: 'public' | 'private' | 'group'. max_members defaults to 100.
{
"channel_name": "general-discussion",
"channel_type": "group",
"max_members": 100
}// 201 Created { "id": "ch_123", "channel_name": "general-discussion", "channel_type": "group", "max_members": 100, "created_at": "2026-03-01T10:00:00Z" }
/api/v1/chat/channelsList all channels belonging to your organization. Optionally filter by channel_type.
// Query params (all optional) ?channel_type=group // filter: public | private | group // Headers Authorization: Bearer hb_live_xxx
{
"channels": [
{
"id": "ch_123",
"channel_name": "general-discussion",
"channel_type": "group",
"member_count": 12,
"created_at": "2026-03-01T10:00:00Z"
}
]
}/api/v1/chat/channels/:channelId/messagesSend a message to a channel. message_type: 'text' | 'image' | 'video' | 'audio' | 'file'. For file messages, set content to the file URL from the Storage API and include metadata.file.
// Text message { "sender_id": "user_123", "content": "Hello World!", "message_type": "text" } // File message (after Storage presign + confirm) { "sender_id": "user_123", "content": "https://cdn.hyperbabel.com/...", "message_type": "image", "metadata": { "file": { "original_name": "photo.jpg", "size_bytes": 524288, "mime_type": "image/jpeg" } } }
// 201 Created { "id": "msg_456", "channel_id": "ch_123", "sender_id": "user_123", "content": "Hello World!", "message_type": "text", "created_at": "2026-03-01T10:01:00Z" }
/api/v1/chat/channels/:channelId/messagesRetrieve paginated message history using cursor-based pagination. Use cursor from the previous response to load the next page. Results are ordered newest-first.
// Query params (all optional, fully combinable) ?limit=50 // messages per page, default: 50 ?cursor=msg_456 // message ID to paginate from (exclusive) // omit for first page (latest messages) // Example — second page: ?limit=50&cursor=msg_456 // Headers Authorization: Bearer hb_live_xxx
{
"messages": [
{
"id": "msg_456",
"channel_id": "ch_123",
"sender_id": "user_123",
"content": "Hello World!",
"message_type": "text",
"translation": {
"en": "Hello World!",
"ja": "こんにちは!"
},
"created_at": "2026-03-01T10:01:00Z"
}
],
"has_more": true,
"next_cursor": "msg_200"
}/api/v1/unitedchat/rooms/:roomId/messages/batch-translateTranslate one or many messages to a target language in a single request. Pass message_ids: [single_id] to translate one message; the same endpoint covers both single and batch use cases. Up to 100 messages per call. Cached translations (per-message JSONB) are returned without re-billing; only cache misses incur translate_chars usage.
{
"message_ids": ["msg_456", "msg_457", "msg_458"],
"target_lang": "en"
}{
"translations": {
"msg_456": "Hello!",
"msg_457": "Hello!"
}
}/api/v1/chat/messages/:messageId/reactionsAdd an emoji reaction to a message. Each user can react once per emoji per message.
{
"user_id": "user_123",
"emoji": "👍"
}{
"message_id": "msg_456",
"emoji": "👍",
"user_id": "user_123",
"created_at": "2026-03-01T10:05:00Z"
}/api/v1/chat/messages/:messageId/reactionsRemove a previously added reaction from a message.
{
"user_id": "user_123",
"emoji": "👍"
}{
"success": true,
"message_id": "msg_456",
"emoji": "👍"
}/api/v1/chat/channels/:channelId/readMark all messages in a channel as read up to the current time for a given user. Updates the unread count to 0.
{
"user_id": "user_123"
}{
"success": true,
"channel_id": "ch_123",
"read_at": "2026-03-01T10:10:00Z"
}📡 Receiving Messages in Real-time (HyperBabel Realtime)
// Subscribe to the channel via HyperBabel Realtime // Channel pattern: chat:{channelId} // Incoming message payload { "event": "chat.message.created", "data": { "id": "msg_456", "channel_id": "ch_123", "sender_id": "user_123", "content": "Hello World!", "message_type": "text", "translation": { "en": "Hello World!", "ja": "こんにちは!" }, "created_at": "2026-03-01T10:01:00Z" } } // If the incoming message language != viewer's language: // → translation field is auto-populated by HyperBabel // → Display translation[viewer_language] instead of content
/api/v1/chat/channels/:channelId/messages/:messageIdSoft-delete a message. Sets deleted_at — the message is excluded from all subsequent getMessages responses. Only the sender, or an admin who has permission, can delete.
{ "user_id": "user_123" }{ "success": true, "message_id": "msg_456", "deleted_at": "2026-03-11T15:00:00Z" }/api/v1/chat/channels/:channelId/messages/:messageIdEdit a text message. Only the original sender can edit. Only text type messages are editable. The original content is saved in original_content for audit. edited_at is set automatically.
{
"user_id": "user_123",
"content": "Updated message content"
}{
"success": true,
"message": {
"id": "msg_456",
"content": "Updated message content",
"original_content": "Original message content",
"edited_at": "2026-03-11T15:01:00Z"
}
}/api/v1/chat/channels/:channelId/searchSearch messages using high-performance full-text search. Always provide created_at_gte + created_at_lte to enable indexing optimizations — max range 6 months, defaults to last 6 months. Results include a highlight field with matched terms wrapped in <mark> tags.
// Query params ?q=hello+world // Required — search query (spaces=AND, quotes=phrase) ?limit=20 // Optional, max 50 ?created_at_gte=2026-03-01T00:00:00Z ?created_at_lte=2026-03-11T23:59:59Z // Response header X-Applied-Created-At-Gte:
{
"messages": [
{
"id": "msg_456",
"sender_id": "user_123",
"content": "Hello World!",
"highlight": "Hello World!",
"created_at": "2026-03-01T10:01:00Z"
}
],
"query": "hello world",
"applied_range": {
"gte": "2025-09-11T23:37:00Z",
"lte": "2026-03-11T23:37:00Z"
}
}/api/v1/chat/channels/:channelId/typingBroadcast a typing event via the real-time channel. No DB storage — fully stateless. Call with is_typing: true when the user starts typing, and is_typing: false when they stop or send a message.
{
"user_id": "user_123",
"is_typing": true
}{ "success": true, "user_id": "user_123", "is_typing": true }
// Real-time event received by other clients:
// event: "typing"
// data: { "user_id": "user_123", "is_typing": true, "timestamp": "..." }/api/v1/chat/channels/:channelId/messages/:messageId/threadGet all reply messages for a parent message (reply_to = messageId). Ordered oldest-first. Supports cursor pagination. created_at_gte defaults to the parent message's created_at automatically.
// Query params (all optional)
?limit=50
?cursor=msg_xxx // last message ID from previous page{
"parent_message": {
"id": "msg_456",
"content": "Hello World!",
"sender_id": "user_123",
"created_at": "2026-03-01T10:01:00Z"
},
"thread": [
{
"id": "msg_457",
"sender_id": "user_124",
"content": "Hi!",
"reply_to": "msg_456",
"created_at": "2026-03-01T10:02:00Z"
}
],
"has_more": false,
"next_cursor": null,
"thread_count": 1
}Lightweight online/offline presence tracking backed by DB heartbeat polling — no additional real-time channel costs. Clients call /heartbeat every 30 seconds. Users with no heartbeat for 90 seconds are considered offline.
/api/v1/presence/heartbeatCall every 30 seconds to stay marked as online. If no heartbeat is received for 90 seconds, the user is automatically considered offline. **Billing.** Each unique (org_id, current_minute, user_id) triggers +1 `connection_min` in the monthly rollup (Redis SADD-deduped — heartbeats more frequent than 1/min are absorbed without double-counting). Heartbeats slower than 1/min may miss minute buckets. Mobile demos auto-pause the heartbeat when the app backgrounds (JS runtime suspension / Swift Task suspension / Compose lifecycle cancellation).
{
"user_id": "user_123",
"device": "mobile" // optional
}{ "success": true, "status": "online" }/api/v1/presence/statusExplicitly set a user's status. Useful for manual away/dnd modes.
{
"user_id": "user_123",
"status": "dnd" // online | away | dnd | offline
}{ "success": true, "user_id": "user_123", "status": "dnd" }/api/v1/presenceGet presence status for up to 100 users at once. Users not found in presence table are returned as offline.
// Query
?user_ids=user_123,user_124,user_125 // comma-separated, max 100{
"presence": [
{ "user_id": "user_123", "status": "online", "last_seen": "2026-03-11T23:55:00Z", "device": "mobile" },
{ "user_id": "user_124", "status": "offline", "last_seen": null, "device": null }
],
"offline_threshold_seconds": 90
}Global user blocking within an organization. When user A blocks user B, any message B tries to send in a shared room is rejected server-side with 403 blocked_by_recipient. The response body includes a blocked_by array with the blocker's user ID. Note: sandbox requests bypass this check.
/api/v1/users/blockBlock a user globally within the org. Once blocked, any message the blocked user sends to a shared room is rejected server-side with 403 blocked_by_recipient. Sandbox requests skip this check.
{
"blocker_id": "user_A",
"blocked_id": "user_B"
}{ "success": true, "blocker_id": "user_A", "blocked_id": "user_B" }/api/v1/users/blockRemove a block between two users.
{
"blocker_id": "user_A",
"blocked_id": "user_B"
}{ "success": true, "blocker_id": "user_A", "blocked_id": "user_B" }/api/v1/users/:userId/blocksGet the list of user IDs that userId has blocked.
// GET — no body{
"user_id": "user_A",
"blocked_users": [
{ "blocked_id": "user_B", "created_at": "2026-03-11T10:00:00Z" }
]
}Register and manage FCM (Android/Web) or APNs (iOS) device tokens. Tokens are used by HyperBabel to deliver push notifications for new messages when users are offline.
💡 To enable actual push delivery, upload your Firebase service account JSON in the console dashboard under Push Settings. Without this, tokens are stored but no pushes are sent.
/api/v1/push/registerRegister a device token. If the same token already exists for this user, updated_at is refreshed.
{
"user_id": "user_123",
"token": "fcm_device_token_here",
"platform": "android" // ios | android | web
}{ "success": true, "user_id": "user_123", "platform": "android" }/api/v1/push/unregisterRemove a device token. Call this on logout or when the token is refreshed.
{
"user_id": "user_123",
"token": "fcm_device_token_here"
}{ "success": true, "user_id": "user_123" }/api/v1/push/tokensGet all registered device tokens for a user.
// Query
?user_id=user_123{
"user_id": "user_123",
"tokens": [
{ "token": "fcm_device_token_here", "platform": "android", "updated_at": "2026-03-11T10:00:00Z" }
]
}Full video call infrastructure with a high-performance media layer and a real-time signaling layer. Supports 1:1 calls and group calls (up to 5 participants total). All events are automatically recorded as chat messages when linked to a chat room.
400 limit_exceeded.1:1 Video Call
Caller → Callee (Invite/Accept/Reject). Either party leaving auto-ends the session via the activeCount ≤ 1 rule.
Group Video Call
Up to 5 participants total. Individual /leave keeps session alive for others. Last active participant leaving auto-ends.
A calls B. B receives a real-time CALL_INVITE signal via HyperBabel Realtime. B accepts or rejects. When either party ends the call, the entire session terminates.
// 1:1 Call Flow (room-scoped)
Caller (A) Callee (B)
| |
|-- POST /rooms/:id/video-call -------->| // CALL_INVITE via HyperBabel Realtime
|<-- /rooms/:id/video-call/accept ------| // B accepts → joins RTC channel
| or /reject (reason: declined | |
| busy | missed) |
|======= RTC Media Stream ==============|
|-- POST /rooms/:id/video-call/leave -->| // Either side calls /leave
| | // → activeCount≤1 → session auto-ends
| | // → call.ended broadcast📊 Incoming Call Lifecycle — Accept / Reject / Busy
When a call is initiated, the callee receives a real-time CALL_INVITE event on their private channel. Depending on the callee's state, one of three paths is taken: accept, reject (or 30 s timeout), or a silent busy rejection when the callee is already in another call.
IncomingCallListener.jsx + IncomingCallOverlay.jsx
✅ Accept
POST /video-call/accept
Callee joins RTC channel
❌ Reject
POST /video-call/reject (reason: declined)
call_rejected logged
⏱ 30 s Timeout
POST /video-call/reject (reason: missed)
call_missed logged · caller exits
📵 Busy (already in a call)
POST /video-call/reject (reason: busy)
call_busy logged · caller toast
/api/v1/unitedchat/rooms/:roomId/video-callInitiate a 1:1 or group video call linked to a chat room. Generates RTC tokens, sends CALL_INVITE signals, records a call_started system message. Pass `quality: 'fhd'` for FHD billing tier; omit (or send `'hd'`) for HD. The value is persisted on the session row and drives whether minutes count toward `hd_video_mins` or `fhd_video_mins` when the call ends. Returns `403 { error: { code: 'quota_exceeded' } }` when the org's free-tier monthly quota is exhausted, before any RTC tokens are issued. Sandbox / `hb_test_*` keys bypass the quota check entirely.
{
"caller_id": "user_A",
"target_user_ids": ["user_B"],
"quality": "hd" // optional — "hd" (default) | "fhd"
}{
"session": {
"id": "sess_789",
"channel_name": "uc-video-mm62pcca",
"call_type": "1to1",
"status": "created",
"participants": [
{ "user_id": "user_A", "uid": 1, "rtc_token": "...", "status": "active" },
{ "user_id": "user_B", "uid": 2, "rtc_token": "...", "status": "invited" }
],
"app_id": ""
}
}/api/v1/unitedchat/rooms/:roomId/video-call/acceptRecord call acceptance. Marks participant status as active. The accepting user joins the RTC media channel using their rtc_token.
{
"user_id": "user_B",
"session_id": "sess_789"
}{
"success": true,
"session_id": "sess_789"
}/api/v1/unitedchat/rooms/:roomId/video-call/rejectRecord a call rejection. Reason: "declined" | "busy" | "missed". A CALL_REJECT or CALL_BUSY signal is sent to the caller via HyperBabel Realtime.
{
"user_id": "user_B",
"session_id": "sess_789",
"reason": "busy"
}{
"success": true
}/api/v1/unitedchat/rooms/:roomId/video-call/leaveEither participant calls /leave when they're done. When active participants drop to 1 or below, the backend automatically marks the session ended, broadcasts call.ended, and records the call_ended chat message with duration. This is the recommended ending pattern for room-scoped 1:1 calls — it works for both caller and callee without requiring host-only permission. **Billing.** When this call ends the session (last active participant left), it writes `hd_video_mins` or `fhd_video_mins` (per session.quality) and bumps `api_calls`. Sandbox-environment sessions skip the monthly rollup. Customer-JWT-authenticated callers (Firebase Direct Exchange mobile apps) are metered identically to API-key callers — this route was the critical fix path for ensuring customer-JWT video calls get billed.
{
"session_id": "sess_789",
"user_id": "user_A"
}// Last active participant — session auto-ends { "success": true, "action": "session_ended", "active_count": 0 }
/api/v1/unitedchat/rooms/:roomId/video-call/endForcefully terminate the session for ALL participants. Permission gate: only the original caller, the room owner, or a sub_admin can call this. Regular participants should use /leave instead — calling /end returns 403 forbidden. Useful for moderators dismissing a runaway group call. **Billing.** Same metering as /leave — records `hd_video_mins` or `fhd_video_mins` (per session.quality) plus an `api_calls` bump. Sandbox skipped from monthly. Customer-JWT and API-key callers metered identically.
{
"session_id": "sess_789",
"user_id": "user_A"
}{
"success": true,
"duration_seconds": 222
}
// Non-host attempt:
// 403 { "error": { "code": "forbidden", "message": "Only the caller or a room owner / sub_admin can end the call for all. Use /leave to drop only yourself." } }A participant can leave individually without ending the session. The session auto-ends when the last active participant leaves.
target_user_ids). Requesting more returns 400 limit_exceeded.// Group Call Flow
Caller (A) Invitee (B) Invitee (C)
|-- POST /video-call ->|-- CALL_INVITE ----->|
|<===== RTC media channel (bidirectional) =====>|
| |
|-- A leaves -------->| | // POST /leave → A status="left"
| |<-- call.participant_left // toast shown to B, C
|-- B leaves -------->| | // POST /leave → 1 active → auto end
| | call.ended | // → C exits/api/v1/unitedchat/rooms/:roomId/video-call/leaveLeave a group video call without ending the session for others. If active participants drop to 1 or below, the session auto-ends and call.ended is published.
{
"session_id": "sess_789",
"user_id": "user_A"
}// Case 1: Others still active { "success": true, "action": "participant_left", "active_count": 2 } // Case 2: Last participant → session auto-ended { "success": true, "action": "session_ended", "active_count": 0 }
/api/v1/unitedchat/rooms/:roomId/video-call/activeCheck if a chat room has an ongoing video call. Show a 'Join ongoing call' banner instead of 'Start call' when active=true.
// No body — GET request // Header: Authorization: Bearer
{
"active": true,
"session": {
"id": "sess_789",
"call_type": "group",
"status": "active",
"active_participant_count": 2,
"participants": [...]
}
}
// or: { "active": false, "session": null }/api/v1/video/sessions/:sessionId/joinJoin an in-progress group call. Generates a new RTC token. If the user previously left, their status is restored to 'active'. Returns 400 if session is already ended.
{
"user_id": "user_D",
"display_name": "Diana"
}{
"session": { "id": "sess_789", "status": "active" },
"token": "" ,
"uid": 4
}/api/v1/unitedchat/rooms/:roomId/video-call/renew-tokenMint a fresh RTC token for the current user's participant slot in the room's active session. Pair this with the media SDK's 'token will expire' callback — the issued RTC tokens last 1 hour, so calls that exceed that (or that get re-joined after a long pause) will otherwise silently drop. The endpoint does not write to the session — only the token is regenerated, with the same uid + channel + publisher role. Returns 403 forbidden if the caller is not a participant of the session, 404 if no active session exists.
{
"user_id": "user_A",
"session_id": "sess_789" // optional — defaults to the room's most recent live session
}{
"session_id": "sess_789",
"channel_name": "uc-video-mm62pcca",
"uid": 1,
"rtc_token": "" ,
"app_id": "" ,
"expires_in": 3600
}HyperBabel includes a built-in real-time signaling layer for call events. Your app receives these events through the HyperBabel Realtime API. A Realtime token scoped to your org is obtained via the token endpoint.
private:user:{orgId}:{callee}Sent to each invitee when a call is initiated. Carries the callee's RTC credentials, the actual call cardinality, and the full target list so the receiving overlay can render its label (e.g. "Group call · with 3 others") without an extra room fetch.
{
"type": "CALL_INVITE",
"sessionId": "sess_789",
"callerId": "user_A",
"callerName": "Alice",
"callType": "1to1" | "group", // call cardinality
"mediaType": "video", // media kind
"channelName": "uc-video-mm62pcca",
"rtcToken": "" ,
"uid": 2,
"app_id": "" ,
"roomId": "room_xyz",
"roomSignalChannel": "" ,
"target_count": 3, // total invitees (excl. caller)
"target_ids": ["user_B","user_C","user_D"],
"target_names": { // { user_id → display_name }
"user_B": "Bob", "user_C": "Carol", "user_D": "Dave"
}
}private:user:{orgId}:{caller}Sent to caller when callee accepts. Caller can now join the RTC media channel.
{
"type": "CALL_ACCEPT",
"session_id": "sess_789",
"user_id": "user_B"
}private:user:{orgId}:{caller}Sent to caller when callee explicitly declines the call.
{
"type": "CALL_REJECT",
"session_id": "sess_789",
"user_id": "user_B"
}private:user:{orgId}:{caller}Sent to caller when callee is already in another call or unavailable.
{
"type": "CALL_BUSY",
"session_id": "sess_789",
"user_id": "user_B"
}hb-video:{sessionId}Broadcast when someone leaves a group call (session still active).
{
"type": "call.participant_left",
"session_id": "sess_789",
"user_id": "user_A",
"active_count": 2
}hb-video:{sessionId}Broadcast when the session ends (1:1 end, or last group participant leaves).
{
"type": "call.ended",
"session_id": "sess_789",
"reason": "last_participant_left"
}Endpoints in this section share a structured error envelope: { "error": { "code": "...", "message": "..." } }. Map these codes in your client so users see the right copy and your app can decide whether to retry, suggest an action, or silently dismiss.
| HTTP | code | Where | Meaning · Recovery |
|---|---|---|---|
| 400 | no_valid_targets | POST /video-call | target_user_ids is empty after deduplication and removing the caller. Ask the user to pick at least one other participant. |
| 400 | limit_exceeded | POST /video-call | More than 4 target_user_ids requested. Cap at 5 participants total (caller + 4). |
| 403 | forbidden | POST /video-call/end | Only the call's caller, the room owner, or a sub_admin can end the session for everyone. Regular participants should call /leave instead — the backend auto-ends when activeCount ≤ 1. |
| 403 | forbidden | POST /video-call/renew-token | The user is not a participant of the targeted session. Re-check session_id / user_id before retrying. |
| 404 | not_found | /video-call/* | The room (or session, for /renew-token) does not exist in this org. Typically a stale URL — refresh the room list. |
| 409 | caller_busy | POST /video-call | The caller is already in another active session in this org (any device). Direct them to end the existing call first. The server checks every active participant across all platforms, so this is authoritative even when the existing call is on another device / browser. |
| 409 | targets_busy | POST /video-call | Every requested target is currently in another active session. Response carries busy_targets[]. For partial busy in group calls, the call proceeds with the available targets and the caller receives CALL_BUSY signals per skipped target — no 409. |
| 410 | session_ended | POST /video-call/accept | The session was wound down between INVITE delivery and the Accept attempt — caller cancelled, cron sweep, or End-for-All by another participant. Dismiss the incoming overlay and show a brief "Call already ended" cue. |
A successful no-op (e.g. /leave on an already-ended session, /end while a /reject was racing) returns200with a message body so the client doesn't have to special-case idempotent retries.
Your app should call /leave or/end whenever a session completes — that's how durations are measured accurately and how chat history records the outcome. But OS process termination, network outages, and force-quits all skip cleanup. HyperBabel runs a server-side sweeper so abandoned sessions don't pin a participant in a permanent "busy" state.
status='created' · 15 min
Sessions where no one ever accepted (caller alone) auto-end 15 minutes after creation. Reason: auto_expired_abandoned.
status='active' · 8 hr
Calls running longer than 8 hours auto-end. Reason:auto_expired. Practically only triggers for orphaned sessions (every participant's app crashed).
When an auto-expire fires, HyperBabel publishescall.ended on the session's signal channel with the matching reason. Treat it identically to a normal end — wind down the local UI, surface the duration if you tracked it. The sweeper does not retroactively bill — billable minutes for a sweep-ended session are computed fromstarted_at to the sweep timestamp.
Full live broadcast infrastructure. Supports host broadcasting and viewer token generation. All endpoints require Authorization: Bearer hb_live_xxx and the live_stream scope.
/api/v1/stream/sessionsList stream sessions for the organisation, filtered by status. Use this to build a live broadcast discovery screen — viewers can see who is currently live and click to join. Ended streams are automatically excluded when using status=live.
// GET — no body // Query params: // status — 'live' | 'created' | 'ended' (default: 'live') // limit — max results (default: 20, max: 100) Authorization: Bearer hb_live_xxx // Example: GET /api/v1/stream/sessions?status=live&limit=20
{
"sessions": [
{
"id": "stream_uuid",
"title": "Live Q&A Session",
"status": "live",
"host": {
"user_id": "host-001",
"display_name": "Yujin"
},
"viewer_count": 42,
"started_at": "2026-03-16T10:00:00Z",
"created_at": "2026-03-16T09:58:00Z"
}
],
"total": 1
}/api/v1/stream/sessionsCreate a new live stream session and generate an RTC publisher token for the host. Optionally provide host display info (name, profile image, preferred language) for rich stream UI. Pass `quality: 'fhd'` for FHD billing; omit (or send `'hd'`) for HD. The value is persisted on the session row and drives whether minutes count toward `hd_stream_mins` or `fhd_stream_mins` when the stream ends. Returns `403 { error: { code: 'quota_exceeded' } }` if the free-tier monthly limit is exhausted, before allocating broadcaster credentials. Sandbox / `hb_test_*` keys bypass this check. **`/start` response note:** the response of `POST /api/v1/stream/sessions/:sessionId/start` includes `sandbox_time_limit_seconds: 900` ONLY for sandbox keys. Live/production keys never receive this field — their streams have no time cap.
{
"host_user_id": "host-001",
"title": "Live Q&A Session",
"host_display_name": "Yujin", // optional
"host_profile_image_url": "https://cdn.example.com/yujin.png", // optional
"host_preferred_lang_cd": "ko", // optional, BCP-47
"quality": "hd" // optional — "hd" (default) | "fhd"
}// 201 Created { "session": { "id": "stream_uuid", "channel_name": "hb:orgid:stream:abc123", "title": "Live Q&A Session", "status": "created", "app_id": "" , "host": { "user_id": "host-001", "display_name": "Yujin", "uid": 10000, "rtc_token": "" } } }
/api/v1/stream/sessions/:sessionId/viewer-tokenGenerate an RTC subscriber token for a viewer. Call this whenever a viewer joins the stream. Optionally provide viewer info to identify them in the stream chat.
{
"user_id": "viewer-002", // optional
"viewer_display_name": "Oliver", // optional
"viewer_profile_image_url": "https://cdn.example.com/oliver.png", // optional
"viewer_preferred_lang_cd": "en" // optional, for translation
}{
"channel_name": "hb:orgid:stream:abc123",
"uid": 10042,
"token": "" ,
"display_name": "Oliver",
"profile_image_url": "https://cdn.example.com/oliver.png",
"preferred_lang_cd": "en",
"app_id": ""
}/api/v1/stream/sessions/:sessionId/startTransition the stream session from 'created' to 'live' status. Call this when the host taps 'Go Live'. Fires a stream.session.started webhook.
// No body required // Headers Authorization: Bearer hb_live_xxx
{
"session_id": "stream_uuid",
"status": "live",
"started_at": "2026-03-10T10:00:00Z"
}/api/v1/stream/sessions/:sessionId/heartbeatHosts MUST POST to this endpoint every ~30 seconds while broadcasting. The server uses the most recent heartbeat to detect a crashed or abandoned host within minutes and end the session automatically — billing then reflects the actual stream duration (last heartbeat → started_at) rather than the 8-hour wall-clock fallback. Fire-and-forget: a single failed heartbeat is non-fatal; stop sending once you call /end.
// No body required // Headers Authorization: Bearer hb_live_xxx
{ "ok": true }/api/v1/stream/sessions/:sessionId/endMark the stream as ended, calculate duration, and record usage. Fires a stream.session.ended webhook. Production streams are billed per minute.
// No body required // Headers Authorization: Bearer hb_live_xxx
{
"session_id": "stream_uuid",
"status": "ended",
"duration_seconds": 3600,
"ended_at": "2026-03-10T11:00:00Z"
}AI-powered translation supporting individual text, batch multi-language, automatic language detection, and supported language lookup. All endpoints require Authorization: Bearer hb_live_xxx and the translate scope.
/api/v1/translate/textTranslate a single text string to a target language. source_language defaults to 'auto' (auto-detect). Returns the detected source language and the input character count for usage transparency.
{
"text": "안녕하세요",
"target_language": "en",
"source_language": "auto" // optional, default: auto-detect
}{
"translated_text": "Hello",
"source_language": "ko",
"target_language": "en",
"char_count": 5
}/api/v1/translate/batchTranslate a single text string to multiple target languages in one request. Ideal for broadcasting a message to a multilingual audience. successful_languages lists the codes that returned a translation (failures, e.g. unsupported pairs, are silently skipped).
{
"text": "안녕하세요",
"target_languages": ["en", "ja", "fr", "zh"],
"source_language": "auto" // optional
}{
"translations": {
"en": "Hello",
"ja": "こんにちは",
"fr": "Bonjour",
"zh": "你好"
},
"char_count": 5,
"successful_languages": ["en", "ja", "fr", "zh"]
}/api/v1/translate/detectDetect the language of a given text string. Returns the detected language code (BCP-47) and a confidence score (0.0–1.0).
{
"text": "안녕하세요"
}{
"detected_language": "ko",
"confidence": 0.99
}/api/v1/translate/languagesGet the list of all supported translation language codes. Use the returned codes as source_language or target_language values. count is the total number of languages in the languages array.
Mobile/web SDKs using a customer JWT should call /api/v1/customer/translate/languages instead — same response, customer-JWT auth so the org API key never ships in the binary.
// GET — no body // Headers Authorization: Bearer hb_live_xxx
{
"languages": [
{ "code": "ko", "name": "Korean" },
{ "code": "en", "name": "English" },
{ "code": "ja", "name": "Japanese" },
{ "code": "zh", "name": "Chinese (Simplified)" },
{ "code": "fr", "name": "French" }
],
"count": 130
}Globally distributed file storage with built-in security. Supports chat file delivery and external API uploads. An allowlist-only security policy is enforced — only approved file types can be uploaded.
Download egress is 100% free.
Folder navigation, file listing, downloads, deletion, and preview (image/video/audio) functions are provided in the Storage Explorer on the console dashboard.
🖼️ Image
jpg, jpeg, png, gif, webp, bmp, svg, ico, tiff, heic
🎬 Video
mp4, webm, ogg, mov, avi, mkv, m4v, flv, wmv
🎵 Audio
mp3, wav, flac, aac, m4a, ogg, opus, wma
📄 Document
pdf, doc, docx, xls, xlsx, ppt, pptx, txt, csv, xml, json, md
📦 Archive
zip, tar, gz, bz2, 7z, rar (max 1 depth)
❌ Blocked
exe, sh, bat, cmd, ps1, php, py, js, ts, dll, bin ...
| Free | Starter | Pro | Business | Business Plus | Enterprise |
|---|---|---|---|---|---|
| 25 MB | 100 MB | 500 MB | 1 GB | 2 GB | 5 GB |
/api/v1/storage/presignPre-validate file type (allowlist), size (plan limit), and storage quota. On success, returns a presigned PUT URL for direct-to-storage upload. The URL is valid for 15 minutes. Free-tier orgs that have exhausted their `monthly_storage_mb` allocation get `403 { error: { code: 'quota_exceeded' } }` BEFORE any bytes hit storage — the upload won't even start. Sandbox / `hb_test_*` keys bypass this check (test traffic is unbilled by policy).
// application/json { "filename": "profile_photo.jpg", "mimeType": "image/jpeg", "fileSize": 524288, // bytes "channelId": "ch_uuid", // for chat uploads "folder": "uploads" // for API uploads (default: "uploads") } // Headers Authorization: Bearer hb_live_xxx
// 201 Created { "message": "Presigned upload URL created", "data": { "upload_url": "https://storage.hyperbabel.com/...?Signature=...", "key": "storage/org_uuid/...", "expires_in": 900, "message_type": "image", "category": "images" } } // 400 — blocked file type { "error": { "code": "blocked_file_type" } } // 400 — file too large { "error": { "code": "file_too_large", "limit_mb": 100 } } // 403 — quota exceeded { "error": { "code": "storage_quota_exceeded" } }
📤 Step 2 — Direct Upload (Client → Storage)
PUT the raw file binary to the upload_url from Step 1. The file goes directly to storage — the HyperBabel server is not involved.
/api/v1/storage/confirmAfter the client finishes uploading to storage, call this endpoint to verify the file exists (via HeadObject), record usage, and get back the final file metadata. If the actual file size exceeds the plan limit, the file is automatically deleted.
// application/json { "key": "/chat/ , // from presign response "originalName": "profile_photo.jpg", // Optional — both fields auto-populate when omitted: // • channel_id: extracted from the key path (chat uploads only) // • uploader_id: customer JWT end-user's external_user_id "channel_id": "/images/..." " , "uploader_id": "alice-uuid" } // Headers Authorization: Bearer
// 200 OK { "message": "Upload confirmed", "data": { "key": "storage/org_uuid/...", "url": "https://cdn.hyperbabel.com/...", "original_name": "profile_photo.jpg", "size_bytes": 524288, "mime_type": "image/jpeg", "message_type": "image" } } // 404 — file not found (upload failed) { "error": { "code": "confirm_failed" } } // 400 — actual file exceeds plan limit (auto-deleted) { "error": { "code": "file_too_large" } }
💬 Chat File Delivery — Full Flow
// 1. Presign POST /api/v1/storage/presign { "filename": "photo.jpg", "mimeType": "image/jpeg", "fileSize": 524288, "channelId": "ch_uuid" } → { "upload_url": "...", "key": "...", "message_type": "image" } // 2. Direct upload to storage PUT {upload_url} Content-Type: image/jpeg (raw file binary) // 3. Confirm upload POST /api/v1/storage/confirm { "key": "...", "originalName": "photo.jpg" } → { "url": "https://cdn.hyperbabel.com/...", "original_name": "photo.jpg", "size_bytes": 524288, "mime_type": "image/jpeg", "message_type": "image" } // 4. Send as chat message POST /api/v1/unitedchat/rooms/{roomId}/messages { "sender_id": "user_123", "content": "{url from step 3}", "message_type": "image", "metadata": { "file": { "original_name": "photo.jpg", "size_bytes": 524288, "mime_type": "image/jpeg" } } }
/api/v1/storage/url/:key(*)Generate a temporary signed URL for a private file. The original filename is automatically set in the Content-Disposition header. Valid for 15 minutes. :key is the full path returned from the upload response.
// GET — no body // Header: Authorization: Bearer hb_live_xxx or JWT // :key = full file path from upload response
{
"url": "https://storage.hyperbabel.com/...?Expires=900&Signature=...",
"expires_in": 900
}