# Messaging API

Send and receive direct messages on Instagram, Facebook, WhatsApp, X, Telegram, TikTok, Slack, iMessage, RCS, SMS and more through one set of conversation endpoints.

import { Tab, Tabs } from 'fumadocs-ui/components/tabs';

The Messaging API sends and receives direct messages. Every channel uses the same conversations and the same send call, so one integration answers DMs everywhere. Public comments on posts are the [Comments API](/products/comments).

## What you can build

- **A support bot or AI agent.** Receive each DM through the [`message.received`](/webhooks/inbox/message-received) webhook and answer with `POST /v1/inbox/conversations/{conversationId}/messages`.
- **Outbound messages to a customer.** Write to someone first with `POST /v1/inbox/conversations`, on the channels that allow it.
- **Rich replies.** Send images and files, buttons, quick replies, cards or WhatsApp templates through the same send call.
- **A chat view in your product.** Read a thread with `GET /v1/inbox/conversations/{conversationId}/messages` and mark it read with `POST /v1/inbox/conversations/{conversationId}/read`.

## Platforms

<ProductPlatforms product="messaging" />

| Platform | Who writes first | Notes |
|---|---|---|
| <PlatformName slug="instagram" page="inbox" /> | The person | Buttons, quick replies and cards. Reply within Meta's window |
| <PlatformName slug="facebook" page="inbox" /> | The person | Messenger. Buttons, quick replies, cards and Page labels |
| <PlatformName slug="whatsapp" page="inbox" /> | Either, you open with a template | Free text inside the 24-hour window, templates outside it |
| <PlatformName slug="twitter" page="inbox" /> | Either | Encrypted X Chat messages are not readable through X's API |
| <PlatformName slug="telegram" page="inbox" /> | The person | Messages go to and from your bot |
| <PlatformName slug="bluesky" page="inbox" /> | Either | Text only |
| <PlatformName slug="reddit" page="inbox" /> | Either | Text only |
| <PlatformName slug="tiktok" page="inbox" /> | The person | Business accounts outside the EEA, Switzerland and the UK. Up to 10 messages within 48 hours |
| <PlatformName slug="slack" page="inbox" /> | Either | DMs and bot mentions in your workspace |
| <PlatformName slug="imessage" page="inbox" /> | Either | New contacts are paced |
| <PlatformName slug="rcs" page="inbox" /> | Either | Rich cards go through [RCS sending](/platforms/rcs/sending) |
| <PlatformName slug="sms" page="sending" /> | Either | From a number you own |

LinkedIn, YouTube, Threads and Pinterest have no DM API open to third-party apps.

## How it works

A **conversation** is one thread between one of your accounts and one person. It carries the `platform`, the `accountId` that owns it, and the participant. Its **messages** have a `direction`, `incoming` or `outgoing`.

Incoming DMs arrive by webhook from most platforms. Bluesky, Reddit and X are polled, so they can take a few minutes. Either way, each one fires [`message.received`](/webhooks/inbox/message-received) with its `conversationId`.

You answer with that `conversationId` and the `accountId` of the receiving account. A `200` means Zernio accepted the message. Delivery arrives later as [`message.sent`](/webhooks/inbox/message-sent), [`message.delivered`](/webhooks/inbox/message-delivered) or [`message.failed`](/webhooks/inbox/message-failed).

## Your first request

You need an API key and a conversation id, from [List conversations](/messages/list-inbox-conversations) or a `message.received` webhook. This replies in that conversation.

<Tabs items={['Node.js', 'Python', 'curl']}>
<Tab value="Node.js">
```typescript
import { randomUUID } from 'crypto';
import Zernio from '@zernio/node';

const zernio = new Zernio();

const { data: sent } = await zernio.messages.sendInboxMessage({
  path: { conversationId: '66c3d2ae7b4f6c8d0e1f2a3b' },
  headers: { 'Idempotency-Key': randomUUID() },
  body: {
    accountId: '66b2e19d8c3f5a7e9d0b1c2d',
    message: 'Yes, still available. Want me to hold it for you?',
  },
});

console.log(sent.data.messageId);
```
</Tab>
<Tab value="Python">
```python
import uuid

from zernio import Zernio

client = Zernio()

sent = client.messages.send_inbox_message(
    conversation_id="66c3d2ae7b4f6c8d0e1f2a3b",
    account_id="66b2e19d8c3f5a7e9d0b1c2d",
    message="Yes, still available. Want me to hold it for you?",
    idempotency_key=str(uuid.uuid4()),
)

print(sent["data"]["messageId"])
```
</Tab>
<Tab value="curl">
```bash
curl -X POST "https://zernio.com/api/v1/inbox/conversations/66c3d2ae7b4f6c8d0e1f2a3b/messages" \
  -H "Authorization: Bearer $ZERNIO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 4b4986f4-77b5-4c22-a3a7-2c56f7a657e1" \
  -d '{ "accountId": "66b2e19d8c3f5a7e9d0b1c2d", "message": "Yes, still available. Want me to hold it for you?" }'
```
</Tab>
</Tabs>

Response (`200`):

```json
{
  "success": true,
  "data": {
    "messageId": "aWdfZAG1faXRlbToxOklHTWVzc2FnZAUlEOjE3ODQx",
    "conversationId": "66c3d2ae7b4f6c8d0e1f2a3b"
  }
}
```

The `Idempotency-Key` makes a retry safe: the same key replays the first answer instead of sending twice ([idempotency](/guides/idempotency)).

## Examples

### Reply to every new DM

Subscribe a URL to `message.received` ([webhooks](/webhooks)). This handler answers each incoming message from the account that received it. Verify the signature first, as shown in [signatures](/webhooks#signatures).

<Tabs items={['Node.js', 'Python']}>
<Tab value="Node.js">
```typescript
import Zernio from '@zernio/node';

const zernio = new Zernio();

export const POST = async (req: Request) => {
  const event = await req.json();
  if (event.event !== 'message.received' || event.message.direction !== 'incoming') {
    return new Response(null, { status: 200 });
  }

  await zernio.messages.sendInboxMessage({
    path: { conversationId: event.message.conversationId },
    headers: { 'Idempotency-Key': event.id },
    body: {
      accountId: event.account.accountId,
      message: `Thanks ${event.message.sender.name}, we got your message.`,
    },
  });

  return new Response(null, { status: 200 });
};
```
</Tab>
<Tab value="Python">
```python
from fastapi import FastAPI, Request, Response
from zernio import Zernio

app = FastAPI()
client = Zernio()

@app.post("/webhooks/zernio")
async def handle_webhook(request: Request):
    event = await request.json()
    if event["event"] != "message.received" or event["message"]["direction"] != "incoming":
        return Response(status_code=200)

    client.messages.send_inbox_message(
        conversation_id=event["message"]["conversationId"],
        account_id=event["account"]["accountId"],
        message=f"Thanks {event['message']['sender']['name']}, we got your message.",
        idempotency_key=event["id"],
    )
    return Response(status_code=200)
```
</Tab>
</Tabs>

Using the event `id` as the `Idempotency-Key` means a redelivered webhook never sends a second reply.

### Send an image

Pass a public `attachmentUrl` and its `attachmentType` (`image`, `video`, `audio` or `file`). `message` is optional with an attachment. Bluesky and Reddit DMs take no attachments, and TikTok takes text or one image.

<Tabs items={['Node.js', 'Python', 'curl']}>
<Tab value="Node.js">
```typescript
await zernio.messages.sendInboxMessage({
  path: { conversationId: '66c3d2ae7b4f6c8d0e1f2a3b' },
  body: {
    accountId: '66b2e19d8c3f5a7e9d0b1c2d',
    message: 'Here is the size chart.',
    attachmentUrl: 'https://example.com/size-chart.png',
    attachmentType: 'image',
  },
});
```
</Tab>
<Tab value="Python">
```python
client.messages.send_inbox_message(
    conversation_id="66c3d2ae7b4f6c8d0e1f2a3b",
    account_id="66b2e19d8c3f5a7e9d0b1c2d",
    message="Here is the size chart.",
    attachment_url="https://example.com/size-chart.png",
    attachment_type="image",
)
```
</Tab>
<Tab value="curl">
```bash
curl -X POST "https://zernio.com/api/v1/inbox/conversations/66c3d2ae7b4f6c8d0e1f2a3b/messages" \
  -H "Authorization: Bearer $ZERNIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "accountId": "66b2e19d8c3f5a7e9d0b1c2d",
    "message": "Here is the size chart.",
    "attachmentUrl": "https://example.com/size-chart.png",
    "attachmentType": "image"
  }'
```
</Tab>
</Tabs>

To upload a file instead of linking it, send the same call as `multipart/form-data` with an `attachment` field. On Instagram and Facebook, image plus text goes out as two messages, listed in `data.messageIds`.

### Start a conversation

[Create conversation](/messages/create-inbox-conversation) writes to someone first. Name them with `participantId` or `participantUsername`. If a conversation with them exists, the message joins it.

<Tabs items={['Node.js', 'Python', 'curl']}>
<Tab value="Node.js">
```typescript
const { data: started } = await zernio.messages.createInboxConversation({
  body: {
    accountId: '66b2e19d8c3f5a7e9d0b1c2f',
    participantUsername: 'jane.bsky.social',
    message: 'Hi Jane, thanks for the mention. Can we send you a sample?',
  },
});
```
</Tab>
<Tab value="Python">
```python
started = client.messages.create_inbox_conversation(
    account_id="66b2e19d8c3f5a7e9d0b1c2f",
    participant_username="jane.bsky.social",
    message="Hi Jane, thanks for the mention. Can we send you a sample?",
)
```
</Tab>
<Tab value="curl">
```bash
curl -X POST "https://zernio.com/api/v1/inbox/conversations" \
  -H "Authorization: Bearer $ZERNIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "accountId": "66b2e19d8c3f5a7e9d0b1c2f",
    "participantUsername": "jane.bsky.social",
    "message": "Hi Jane, thanks for the mention. Can we send you a sample?"
  }'
```
</Tab>
</Tabs>

On WhatsApp, open with an approved template: send `templateName`, `templateLanguage` and `templateParams` instead of `message`.

## How it behaves

- **Same send call, more actions.** You can also [react](/messages/add-message-reaction), [show typing](/messages/send-typing-indicator), and [edit](/messages/edit-inbox-message) or [delete](/messages/delete-inbox-message) where the platform allows.
- **Platform windows apply.** Meta, WhatsApp and TikTok limit how long after the person's last message you can reply freely. The platform page gives each window.
- **Meta history is replayed.** When an Instagram or Facebook account connects, up to 500 older conversations are imported, without webhooks.
- **Billing.** Sent DMs count toward [outbound messages](/pricing/inbox): 10,000 free per month, then $1 per 10,000. X DMs and SMS bill separately.
- **Automate replies** with [workflows](/products/automations/workflows) instead of answering every message from your server.

## If it fails

| Error | Cause | Fix |
|---|---|---|
| `400` `PLATFORM_NOT_SUPPORTED` on create conversation | The platform does not let a business write first | Wait for the person to message you |
| `400` `TEMPLATE_REQUIRED` | A WhatsApp conversation opened without a template | Send `templateName`, `templateLanguage` and `templateParams` |
| `403` `message_allowance_exceeded` | 10,000 messages sent this month with no card on file | Add a payment method |
| `409` `not_thread_owner` | Another app controls this Meta conversation | Take the thread back ([handover](/guides/manychat-patterns#handover-with-another-app)) |
| `429` | Too many requests for your key | Wait for `Retry-After`, then retry with the same `Idempotency-Key` |

## Related

- [Send message](/messages/send-inbox-message)
- [Create conversation](/messages/create-inbox-conversation)
- [List conversations](/messages/list-inbox-conversations)
- [Inbox API](/products/inbox)
- [Inbox webhooks](/webhooks/inbox)
- [Build an inbox for your customers](/multi-tenant/inbox)