Skip to content
BuzzTake
MenuClose
DocumentationDirect messages

How to read and send direct messages with an API

Last updated

Read an account's inbox, open one conversation, and answer it or start a new one. BuzzTake uses the account's own messages page, so the messages come from the account itself.

You need a BuzzTake API key and the id of an active account on a platform with direct messages. Threads and YouTube have none.

1. List the conversations

POST/v1/accounts/{id}/actions

list_conversations reads the inbox, newest first. limit is 20 by default and at most 100. Reads cost 4 credits and are charged only when they succeed.

Request
curl https://api.buzztake.com/v1/accounts/$ACCOUNT_ID/actions \
  -H "Authorization: Bearer $BUZZTAKE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "type": "list_conversations", "limit": 20, "wait": 60 }'
200 OK (trimmed)
{
  "type": "list_conversations",
  "status": "succeeded",
  "result": {
    "type": "conversations",
    "items": [
      {
        "url": "https://www.instagram.com/direct/t/1101.../",
        "id": "1101...",
        "title": "Jamie",
        "last_message": "Is there a student discount?",
        "updated_at": "2026-10-30T07:41:00Z",
        "unread": true
      }
    ]
  },
  "credits": 4
}

Each conversation has url, id, title, last_message, updated_at and unread. Any of them is null when the page does not show it.

2. Read one conversation

Pass the conversation's url or id in conversation. A handle such as @jamie.codes opens the conversation with that person.

Request
curl https://api.buzztake.com/v1/accounts/$ACCOUNT_ID/actions \
  -H "Authorization: Bearer $BUZZTAKE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "read_conversation",
    "conversation": "https://www.instagram.com/direct/t/1101.../",
    "limit": 20,
    "wait": 60
  }'
200 OK (trimmed)
{
  "result": {
    "type": "messages",
    "items": [
      {
        "id": "aWdf...",
        "sender": "jamie.codes",
        "from_me": false,
        "text": "Is there a student discount?",
        "sent_at": "2026-10-30T07:41:00Z",
        "media_urls": []
      }
    ]
  }
}

from_me is true for the account's own messages. limit keeps the newest messages that the page has loaded.

3. Reply in the conversation

send_message takes to and text, and media where the platform allows it. Send the conversation's URL in to to answer in that conversation. A message costs 10 credits, charged when it starts.

curl

curl
curl https://api.buzztake.com/v1/accounts/$ACCOUNT_ID/actions \
  -H "Authorization: Bearer $BUZZTAKE_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: dm-1101-2026-10-30" \
  -d '{
    "type": "send_message",
    "to": "https://www.instagram.com/direct/t/1101.../",
    "text": "Yes: 50% off with a school email. Reply with yours and I will set it up.",
    "wait": 60
  }'

JavaScript

JavaScript
const api = "https://api.buzztake.com/v1";
const accountId = process.env.ACCOUNT_ID;
const headers = {
  Authorization: `Bearer ${process.env.BUZZTAKE_API_KEY}`,
  "Content-Type": "application/json",
};

async function run(body, idempotencyKey) {
  const res = await fetch(`${api}/accounts/${accountId}/actions`, {
    method: "POST",
    headers: idempotencyKey ? { ...headers, "Idempotency-Key": idempotencyKey } : headers,
    body: JSON.stringify({ ...body, wait: 90 }),
  });
  if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
  const action = await res.json();
  if (action.status !== "succeeded") {
    throw new Error(`${body.type} ${action.status}: ${action.error?.message ?? ""}`);
  }
  return action.result;
}

// Your code or your model decides the answer. Return null to skip the conversation.
function draftReply(messages) {
  const question = messages.find((message) => !message.from_me && /discount/i.test(message.text ?? ""));
  return question ? "Yes: 50% off with a school email." : null;
}

const { items: conversations } = await run({ type: "list_conversations", limit: 20 });
for (const conversation of conversations.filter((item) => item.unread)) {
  const ref = conversation.url ?? conversation.id;
  const { items: messages } = await run({ type: "read_conversation", conversation: ref, limit: 20 });
  const text = draftReply(messages);
  if (!text) continue;
  const sent = await run({ type: "send_message", to: ref, text }, `dm-${conversation.id}-${conversation.updated_at}`);
  console.log(conversation.title, "->", sent.url ?? "sent");
}

Python

Python
import os
import re
import requests

api = "https://api.buzztake.com/v1"
account_id = os.environ["ACCOUNT_ID"]
headers = {"Authorization": f"Bearer {os.environ['BUZZTAKE_API_KEY']}"}


def run(body, idempotency_key=None):
    extra = {"Idempotency-Key": idempotency_key} if idempotency_key else {}
    resp = requests.post(
        f"{api}/accounts/{account_id}/actions",
        headers={**headers, **extra},
        json={**body, "wait": 90},
        timeout=100,
    )
    if not resp.ok:
        raise SystemExit(f"{resp.status_code}: {resp.text}")
    action = resp.json()
    if action["status"] != "succeeded":
        raise SystemExit(f"{body['type']} {action['status']}: {action.get('error')}")
    return action["result"]


def draft_reply(messages):
    """Your code or your model decides the answer. Return None to skip the conversation."""
    for message in messages:
        if not message["from_me"] and re.search("discount", message["text"] or "", re.I):
            return "Yes: 50% off with a school email."
    return None


conversations = run({"type": "list_conversations", "limit": 20})["items"]
for conversation in [c for c in conversations if c["unread"]]:
    ref = conversation["url"] or conversation["id"]
    messages = run({"type": "read_conversation", "conversation": ref, "limit": 20})["items"]
    text = draft_reply(messages)
    if not text:
        continue
    key = f"dm-{conversation['id']}-{conversation['updated_at']}"
    sent = run({"type": "send_message", "to": ref, "text": text}, key)
    print(conversation["title"], "->", sent["url"] or "sent")

The scripts read each unread conversation and send an answer when draftReply returns one. The Idempotency-Key includes the conversation's updated_at, so a second run over the same unread message sends nothing new.

Tip: Over MCP, the same three tools are list_conversations, read_conversation and send_message. An agent can draft each answer and send it only after you confirm.

4. Start a new conversation

To message someone the account has no conversation with, send their handle in to, with @ in front. Do not send a profile URL: BuzzTake reads every URL in to as a conversation, and profile URLs fail on X, Instagram, Facebook, LinkedIn and Bluesky. The @ also matters for handles with a hyphen, such as @jamie-doe, which BuzzTake would otherwise read as a conversation id.

A first message
curl https://api.buzztake.com/v1/accounts/$ACCOUNT_ID/actions \
  -H "Authorization: Bearer $BUZZTAKE_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: welcome-jamie.codes" \
  -d '{ "type": "send_message", "to": "@jamie.codes", "text": "Thanks for the follow. Questions are welcome here.", "wait": 60 }'
PlatformWhat to expect
XFrom the profile's Message button. Fails when the person does not take messages from the account.
InstagramNew conversations, text and media.
FacebookMessenger, from the profile's Message button.
LinkedInText only. People outside the account's network can need InMail, which is LinkedIn Premium.
BlueskyText. Media in a message is dropped.
TikTok, Pinterest, Reddit, TelegramText only. On Reddit, messages go to people, not communities.
MastodonA private mention: a direct post to the person.
Threads, YouTubeNo direct messages. The actions answer 422 unsupported.

Common errors

422 invalid_input
"send_message needs text, media, or both"
422 unsupported
"Threads accounts cannot send_message. They can: ..."
Action error rejected
"the platform rejected the action: this account does not take messages from you" on X or Bluesky, or "messaging this member needs InMail (LinkedIn Premium)" on LinkedIn.
Action error invalid_input
"invalid input: LinkedIn messages take text only here". TikTok, Pinterest, Reddit and Telegram refuse media the same way. Remove media; the credits are refunded.
A passcode or PIN prompt
X Chat can ask for the account's 4-digit chat passcode. The action then waits with statusneeds_input: send the passcode with answer_challenge (MCP) orPOST /v1/actions/{id}/answer, and the action goes on. Messenger can ask for the chat history PIN: open a sign-in link and enter it once in the live browser, then send again.

See also

To run this on your own accounts, create a BuzzTake account, then add an account and an API key.