Amba

Messaging

In-app direct and group messaging between users, with typed errors and SDK examples.

Amba provides in-app messaging for direct and group conversations between users. Every endpoint returns a typed error envelope — no opaque 500s.

SDK example: 1:1 conversation

The common shape — two users, one conversation, a thread of messages:

import { Amba } from '@layers/amba-web';
 
// Create a 1:1 conversation between the signed-in user and another
// user. For `type: 'direct'` the call is idempotent — calling it
// again with the same other-participant returns the existing row.
const conversation = await Amba.messaging.createConversation({
  participant_ids: [otherUserId],
  type: 'direct',
});
 
// Send a message into that conversation
const message = await Amba.messaging.sendMessage(conversation.id, {
  body: 'hey, want to start a daily streak together?',
});
 
// List the user's conversations (renders an inbox / chat list)
const conversations = await Amba.messaging.conversations();
for (const c of conversations) {
  console.log(c.id, c.last_message?.body);
}
 
// Page through messages in a conversation (renders the thread view)
const messages = await Amba.messaging.listMessages(conversation.id, {
  limit: 50,
});
 
// Mark the conversation as read once the user has scrolled the thread
await Amba.messaging.markRead(conversation.id);

The full messaging surface — createConversation, conversations, sendMessage, listMessages, getMessage, markRead — ships in @layers/amba-web@4.0.0+, @layers/amba-node@4.0.0+, @layers/amba-react-native@4.0.0+, @layers/amba-expo@4.0.0+, plus the native iOS, Android, Flutter, and Unity SDKs.

Live messages. Instead of polling listMessages, subscribe to a conversation and render new messages the moment they're sent with Amba.messaging.conversation(id).subscribe(...). See Live Updates.

Typed errors

Branch on error.code for actionable handling:

import { AmbaApiError } from '@layers/amba-web';
 
async function startConversation(otherUserId: string, firstMessage: string) {
  let conv;
  try {
    conv = await Amba.messaging.createConversation({
      participant_ids: [otherUserId],
      type: 'direct',
    });
  } catch (err) {
    if (err instanceof AmbaApiError) {
      if (err.code === 'USER_NOT_FOUND') {
        // One of the participant_ids doesn't exist on this project —
        // show "invite them to Amba" UI.
        return { ok: false, reason: 'recipient_not_on_amba' };
      }
      if (err.code === 'INVALID_PAYLOAD') {
        // participant_ids was empty, malformed, or the type field was bad.
        // err.details.missing lists the missing fields.
        return { ok: false, reason: 'bad_input', details: err.details };
      }
    }
    throw err;
  }
 
  await Amba.messaging.sendMessage(conv.id, { body: firstMessage });
  return { ok: true, conversationId: conv.id };
}

Error codes the messaging surface returns:

CodeWhen
INVALID_JSONRequest body wasn't parseable JSON.
INVALID_PAYLOADRequired field missing or malformed. error.details.missing lists the fields.
USER_NOT_FOUNDOne or more participant_ids don't exist. error.details.missing_user_ids enumerates them.
INVALID_CONVERSATION_IDConversation id isn't a valid UUID.
CONVERSATION_NOT_FOUNDConversation doesn't exist.
NOT_A_PARTICIPANTCaller isn't in the conversation's participant list.

Client API reference

MethodPathDescription
POST/client/messaging/conversationsCreate a conversation
GET/client/messaging/conversationsList user's conversations
POST/client/messaging/conversations/:id/messagesSend a message (optional parent_message_id + attachments)
GET/client/messaging/conversations/:id/messagesGet messages (paginated; ?parent_message_id filters a thread)
POST/client/messaging/conversations/:id/typingBroadcast an ephemeral typing signal
POST/client/messaging/conversations/:id/readMark conversation as read

Create a conversation

POST /client/messaging/conversations
{
  "participant_ids": ["user_xxx", "user_yyy"],
  "type": "direct"
}

Send a message

A message can carry an optional threaded-reply parent and an optional list of attachments alongside its body:

POST /client/messaging/conversations/:id/messages
{
  "body": "Hey, want to do a workout challenge together?",
  "metadata": {},
  "parent_message_id": null,
  "attachments": [
    { "url": "https://cdn.example.com/clip.mp4", "content_type": "video/mp4", "size_bytes": 184320 }
  ]
}

Threaded replies

Pass parent_message_id (a message id in the same conversation) to post a reply that threads off another message. List a thread by filtering the message list with ?parent_message_id=<id>; omit it to list every message in the conversation. Each message in the list response carries a reply_count — how many messages thread off it — so you can render "12 replies" without a second call.

POST /client/messaging/conversations/:id/messages   { "body": "💪", "parent_message_id": "msg_abc" }
GET  /client/messaging/conversations/:id/messages?parent_message_id=msg_abc

A parent_message_id that isn't a UUID returns 400 INVALID_PARENT; one that doesn't reference a message in this conversation returns 404 PARENT_NOT_FOUND (you can't reply across conversations).

Attachments

attachments is an array of { url, content_type?, size_bytes?, metadata? } — each entry needs a non-empty url (400 INVALID_ATTACHMENTS otherwise). Pair this with Media: upload the file, then send the message with the returned URL. Attachments come back inline on each message in the list response as attachments: [{ id, url, content_type, size_bytes, metadata }].

Typing signal

Broadcast an ephemeral "this user is typing" signal to the other participants over the live connection. Nothing is persisted — it's fire-and-forget and returns 202.

POST /client/messaging/conversations/:id/typing

Membership-gated like send and list (403 NOT_A_PARTICIPANT otherwise).

React to a message

Add an emoji reaction (idempotent — re-adding the same emoji is a no-op), remove your own, or list the per-emoji counts. Reaction summaries also come back inline on each message in the list response as reactions: [{ emoji, count, reacted }], where reacted is whether the requesting user reacted with that emoji.

POST   /client/messaging/conversations/:id/messages/:messageId/reactions   { "emoji": "🔥" }
DELETE /client/messaging/conversations/:id/messages/:messageId/reactions?emoji=🔥
GET    /client/messaging/conversations/:id/messages/:messageId/reactions

All three require the caller to be a participant in the conversation (403 NOT_A_PARTICIPANT otherwise; 404 MESSAGE_NOT_FOUND if the message isn't in that conversation).

MCP tools

ToolDescription
amba_messaging_get_statsGet messaging statistics (total conversations, messages, active count)

On this page