Skip to content

Two-Way Inbox ​

Read and reply to inbound replies from your customers — the two-way messaging inbox. A conversation groups every inbound message from one customer (on any channel) together with the replies your workspace sends back.

Not the support inbox

This is the customer-facing inbox only. YeboLink also has an internal support inbox for your own team's threads with YeboLink support — that lives under a separate, dashboard-only /support API and is never returned here.

Authentication ​

All five endpoints accept either auth method (authenticateAny):

  • API Key — X-API-Key: ybk_... — the scope required is noted per endpoint below (read_messages or send_messages)
  • JWT Token — Authorization: Bearer <token> — full dashboard access

Unread state ​

unread is computed per request, not stored — a conversation is unread when it holds an inbound message newer than the thread's read watermark (last_read_at). Nothing marks a thread read as a side effect of listing or fetching it:

This is deliberate: if fetching a thread's messages marked it read, a client that polls the inbox on an interval would silently drain its own unread count without a human ever having looked at the thread.

unread_count (returned by the list endpoint) is the workspace-wide total across every customer-facing conversation — it feeds the dashboard's sidebar badge, so it's independent of the current page and of the status filter. It doesn't drop when you page through the list or switch to the Resolved tab.

List Conversations ​

Get a paginated list of your workspace's conversations, newest activity first.

Endpoint ​

GET /api/v1/inbox/conversations

Scope: read_messages

Query Parameters ​

ParameterTypeRequiredDescription
pagenumberNoPage number (default: 1)
limitnumberNoResults per page (default: 50, max: 100)
statusstringNoFilter by open or resolved. An unrecognized value returns 400, not an unfiltered list.

Example Requests ​

bash
# All conversations
curl "https://api.yebolink.com/api/v1/inbox/conversations" \
  -H "X-API-Key: ybk_live_your_api_key"

# Only open threads
curl "https://api.yebolink.com/api/v1/inbox/conversations?status=open&page=1&limit=20" \
  -H "X-API-Key: ybk_live_your_api_key"
javascript
const listConversations = async (filters = {}) => {
  const params = new URLSearchParams(filters);

  const response = await fetch(
    `https://api.yebolink.com/api/v1/inbox/conversations?${params}`,
    { headers: { 'X-API-Key': process.env.YEBOLINK_API_KEY } }
  );

  return await response.json();
};

// Usage
const open = await listConversations({ status: 'open' });
console.log('Unread across the workspace:', open.data.unread_count);
python
import os
import requests

def list_conversations(status: str = None, page: int = 1, limit: int = 50):
    params = {'page': page, 'limit': limit}
    if status:
        params['status'] = status

    response = requests.get(
        'https://api.yebolink.com/api/v1/inbox/conversations',
        headers={'X-API-Key': os.environ.get('YEBOLINK_API_KEY')},
        params=params
    )

    return response.json()

# Usage
open_conversations = list_conversations(status='open')

Response ​

json
{
  "success": true,
  "data": {
    "conversations": [
      {
        "id": "550e8400-e29b-41d4-a716-446655440000",
        "workspace_id": "workspace-id",
        "contact_id": "contact-id",
        "customer_identifier": "+1234567890",
        "channels_used": ["sms"],
        "status": "open",
        "assigned_to": null,
        "last_message_at": "2025-11-02T12:00:00Z",
        "last_read_at": null,
        "last_message_preview": "Hey, is my order shipping today?",
        "created_at": "2025-10-30T09:00:00Z",
        "unread": true
      }
    ],
    "unread_count": 4,
    "pagination": {
      "total": 37,
      "page": 1,
      "pages": 2,
      "limit": 20
    }
  }
}

Error Responses ​

Invalid status filter (400)

json
{
  "success": false,
  "error": "Invalid status filter. Expected one of: open, resolved"
}

Get Conversation Messages ​

Retrieve the full thread — inbound and outbound messages — for one conversation, oldest first.

Does not mark the thread read

Fetching a conversation's messages never touches its read watermark, even though you're "reading" it. This keeps a background poll from silently clearing the unread badge for a thread nobody actually looked at — call POST /conversations/:id/read explicitly when your client displays the thread to a human.

Endpoint ​

GET /api/v1/inbox/conversations/:id/messages

Scope: read_messages

Parameters ​

ParameterTypeRequiredDescription
idstringYesConversation ID (UUID)
limitnumberNoMax messages to return (default: 100, max: 200)

Example Requests ​

bash
curl "https://api.yebolink.com/api/v1/inbox/conversations/550e8400-e29b-41d4-a716-446655440000/messages" \
  -H "X-API-Key: ybk_live_your_api_key"
javascript
const getConversationMessages = async (conversationId) => {
  const response = await fetch(
    `https://api.yebolink.com/api/v1/inbox/conversations/${conversationId}/messages`,
    { headers: { 'X-API-Key': process.env.YEBOLINK_API_KEY } }
  );

  if (response.status === 404) {
    throw new Error('Conversation not found');
  }

  return await response.json();
};
python
def get_conversation_messages(conversation_id: str):
    response = requests.get(
        f'https://api.yebolink.com/api/v1/inbox/conversations/{conversation_id}/messages',
        headers={'X-API-Key': os.environ.get('YEBOLINK_API_KEY')}
    )

    if response.status_code == 404:
        raise Exception('Conversation not found')

    return response.json()

Response ​

json
{
  "success": true,
  "data": {
    "conversation": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "workspace_id": "workspace-id",
      "customer_identifier": "+1234567890",
      "channels_used": ["sms"],
      "status": "open",
      "last_message_at": "2025-11-02T12:00:00Z",
      "last_read_at": "2025-11-02T11:00:00Z",
      "last_message_preview": "Hey, is my order shipping today?",
      "created_at": "2025-10-30T09:00:00Z"
    },
    "messages": [
      {
        "id": "8a1b2c3d-0000-4000-8000-000000000001",
        "workspace_id": "workspace-id",
        "conversation_id": "550e8400-e29b-41d4-a716-446655440000",
        "channel": "sms",
        "direction": "inbound",
        "sender": "+1234567890",
        "recipient": "+15005550006",
        "content_type": "text",
        "content": { "text": "Hey, is my order shipping today?" },
        "status": "delivered",
        "credits_used": 0,
        "created_at": "2025-11-02T12:00:00Z"
      },
      {
        "id": "8a1b2c3d-0000-4000-8000-000000000002",
        "workspace_id": "workspace-id",
        "conversation_id": "550e8400-e29b-41d4-a716-446655440000",
        "api_key_id": "api-key-id",
        "channel": "sms",
        "direction": "outbound",
        "sender": "+15005550006",
        "recipient": "+1234567890",
        "content_type": "text",
        "content": { "text": "Yes! It ships today, tracking lands in your inbox tonight." },
        "provider": "twilio",
        "provider_message_id": "SM1234567890",
        "status": "delivered",
        "credits_used": 1,
        "metadata": {},
        "created_at": "2025-11-02T12:05:00Z"
      }
    ]
  }
}

Error Responses ​

Conversation Not Found (404)

json
{
  "success": false,
  "error": "Conversation not found"
}

A conversation ID that belongs to another workspace, and the ID of an internal support thread, both return this same 404 — neither leaks that the row exists.

Reply to a Conversation ​

Send a reply to the customer on this thread. This goes through the normal send path and is charged credits like any other message.

Endpoint ​

POST /api/v1/inbox/conversations/:id/reply

Scope: send_messages

Request Body ​

ParameterTypeRequiredDescription
textstringYes*Reply body. *Also accepted as content.text — either form works.
content.textstringYes*Same as text, nested form.
content.media_urlsarrayNoMedia attachment URLs (channel-dependent support)
channelstringNoOverrides the channel to reply on. Defaults to the channel of the conversation's most recent message.

Example Requests ​

bash
curl -X POST "https://api.yebolink.com/api/v1/inbox/conversations/550e8400-e29b-41d4-a716-446655440000/reply" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: ybk_live_your_api_key" \
  -d '{
    "text": "Yes! It ships today, tracking lands in your inbox tonight."
  }'
javascript
const replyToConversation = async (conversationId, text) => {
  const response = await fetch(
    `https://api.yebolink.com/api/v1/inbox/conversations/${conversationId}/reply`,
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-API-Key': process.env.YEBOLINK_API_KEY
      },
      body: JSON.stringify({ text })
    }
  );

  return await response.json();
};

// Usage
await replyToConversation(
  '550e8400-e29b-41d4-a716-446655440000',
  'Yes! It ships today, tracking lands in your inbox tonight.'
);
python
def reply_to_conversation(conversation_id: str, text: str):
    response = requests.post(
        f'https://api.yebolink.com/api/v1/inbox/conversations/{conversation_id}/reply',
        headers={'X-API-Key': os.environ.get('YEBOLINK_API_KEY')},
        json={'text': text}
    )

    return response.json()

# Usage
reply_to_conversation(
    '550e8400-e29b-41d4-a716-446655440000',
    'Yes! It ships today, tracking lands in your inbox tonight.'
)

Response ​

201 Created — the standard message object, same shape as POST /api/v1/messages/send:

json
{
  "success": true,
  "data": {
    "message": {
      "id": "8a1b2c3d-0000-4000-8000-000000000002",
      "workspace_id": "workspace-id",
      "conversation_id": "550e8400-e29b-41d4-a716-446655440000",
      "channel": "sms",
      "direction": "outbound",
      "sender": "+15005550006",
      "recipient": "+1234567890",
      "content_type": "text",
      "content": { "text": "Yes! It ships today, tracking lands in your inbox tonight." },
      "status": "queued",
      "credits_used": 1,
      "metadata": {},
      "created_at": "2025-11-02T12:05:00Z"
    }
  }
}

Marks the thread read — but only on success

A successful reply marks the conversation read, on the theory that answering a thread means you've read it. This happens after the send completes, so a failed send (e.g. insufficient credits) leaves the thread unread and still in your triage queue instead of silently disappearing from the badge count.

Error Responses ​

Reply Text Required (400)

json
{
  "success": false,
  "error": "Reply text is required"
}

No Channel Could Be Determined (400)

json
{
  "success": false,
  "error": "Could not determine a channel for this conversation"
}

Insufficient Credits (402)

json
{
  "success": false,
  "error": "Insufficient credits"
}

Conversation Not Found (404)

json
{
  "success": false,
  "error": "Conversation not found"
}

Same anti-leak behavior as the other endpoints: another workspace's conversation and an internal support thread both 404.

Mark a Conversation Read ​

Explicitly move the conversation's read watermark to now, clearing its unread state and removing it from unread_count.

Endpoint ​

POST /api/v1/inbox/conversations/:id/read

Scope: read_messages — not send_messages. Marking a thread read is part of reading your inbox, so a read-only key can still clear an unread count it can see.

Example Requests ​

bash
curl -X POST "https://api.yebolink.com/api/v1/inbox/conversations/550e8400-e29b-41d4-a716-446655440000/read" \
  -H "X-API-Key: ybk_live_your_api_key"
javascript
const markConversationRead = async (conversationId) => {
  const response = await fetch(
    `https://api.yebolink.com/api/v1/inbox/conversations/${conversationId}/read`,
    {
      method: 'POST',
      headers: { 'X-API-Key': process.env.YEBOLINK_API_KEY }
    }
  );

  return await response.json();
};
python
def mark_conversation_read(conversation_id: str):
    response = requests.post(
        f'https://api.yebolink.com/api/v1/inbox/conversations/{conversation_id}/read',
        headers={'X-API-Key': os.environ.get('YEBOLINK_API_KEY')}
    )

    return response.json()

Response ​

json
{
  "success": true,
  "data": {
    "conversation": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "workspace_id": "workspace-id",
      "customer_identifier": "+1234567890",
      "channels_used": ["sms"],
      "status": "open",
      "last_message_at": "2025-11-02T12:00:00Z",
      "last_read_at": "2025-11-02T12:10:00Z",
      "last_message_preview": "Hey, is my order shipping today?",
      "created_at": "2025-10-30T09:00:00Z",
      "unread": false
    }
  }
}

Error Responses ​

Conversation Not Found (404)

json
{
  "success": false,
  "error": "Conversation not found"
}

Update Conversation Status ​

Triage a conversation: resolve it (handled, hidden from the default Open view) or reopen it.

Endpoint ​

PATCH /api/v1/inbox/conversations/:id

Scope: send_messages — resolving a thread is a triage decision visible to your whole team, so a read-only key cannot make it.

Request Body ​

ParameterTypeRequiredDescription
statusstringYesopen or resolved

Example Requests ​

bash
curl -X PATCH "https://api.yebolink.com/api/v1/inbox/conversations/550e8400-e29b-41d4-a716-446655440000" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: ybk_live_your_api_key" \
  -d '{ "status": "resolved" }'
javascript
const setConversationStatus = async (conversationId, status) => {
  const response = await fetch(
    `https://api.yebolink.com/api/v1/inbox/conversations/${conversationId}`,
    {
      method: 'PATCH',
      headers: {
        'Content-Type': 'application/json',
        'X-API-Key': process.env.YEBOLINK_API_KEY
      },
      body: JSON.stringify({ status })
    }
  );

  return await response.json();
};

// Usage
await setConversationStatus('550e8400-e29b-41d4-a716-446655440000', 'resolved');
python
def set_conversation_status(conversation_id: str, status: str):
    response = requests.patch(
        f'https://api.yebolink.com/api/v1/inbox/conversations/{conversation_id}',
        headers={'X-API-Key': os.environ.get('YEBOLINK_API_KEY')},
        json={'status': status}
    )

    return response.json()

# Usage
set_conversation_status('550e8400-e29b-41d4-a716-446655440000', 'resolved')

Response ​

json
{
  "success": true,
  "data": {
    "conversation": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "workspace_id": "workspace-id",
      "customer_identifier": "+1234567890",
      "channels_used": ["sms"],
      "status": "resolved",
      "last_message_at": "2025-11-02T12:00:00Z",
      "last_read_at": "2025-11-02T12:15:00Z",
      "last_message_preview": "Hey, is my order shipping today?",
      "created_at": "2025-10-30T09:00:00Z"
    }
  }
}

Resolving also marks the thread read

Setting status: "resolved" stamps the read watermark too — otherwise a thread resolved straight from the list, without ever being opened, would stay unread forever: counted in the badge but filtered out of the default Open view, with no way to clear it. Reopening (status: "open") leaves the watermark alone.

A resolved thread reopens itself on a new reply

If the customer replies again, the conversation's status flips back to open automatically — a resolved thread is not a permanently closed one. You don't need to (and can't meaningfully) keep something resolved once new inbound arrives.

Error Responses ​

Invalid or Missing Status (400)

json
{
  "success": false,
  "error": "status is required and must be one of: open, resolved"
}

Conversation Not Found (404)

json
{
  "success": false,
  "error": "Conversation not found"
}

Next Steps ​

Built with VitePress