# Send a message

`POST https://api.twittrapi.com/client/dm/send`

Send an encrypted text message to a conversation or to a user.

HTML page: https://www.twittrapi.com/docs/account-actions/client-dm-send

> Account actions run with the session cookie of your own X account and go out through your own proxy. Use only accounts you control and follow X's rules; automated posting, following or liking can get the account limited or suspended. Treat the cookie like a password. Trial keys cannot call these endpoints.

The message is encrypted and signed with your chat key, like on x.com. The first message to a person you have not written to yet starts the encrypted conversation; that person needs X Chat (encrypted messages) on their account. Text only, no media.

## Parameters

| Name | In | Type | Required | Default | Description |
| --- | --- | --- | --- | --- | --- |
| `cookie` | body | string | yes |  | Cookie header of your own logged-in X session. Must contain auth_token, ct0 and twid (your account ID, set by x.com at login); guest_id is used when present. |
| `proxy` | body | string | yes |  | Your proxy, format http://user:pass@host:port. The requests to X go out through it. Used only for this request. |
| `passcode` | body | string | yes |  | Your X Chat passcode, the one you set up for encrypted messages on X. Send it as a string (a number works too, but loses leading zeros), up to 64 characters. It unlocks your chat keys for this request and is not saved. The unlocked keys stay in the server's memory for up to 30 minutes, so follow-up calls need no new unlock. |
| `conversation_id` | body | string | no |  | ID of the conversation, as returned by /client/dm/inbox. A one-to-one chat has the ID <smaller user ID>:<larger user ID>. Letters, digits, colon, hyphen and underscore, up to 128 characters. Either conversation_id or user_id is required. |
| `user_id` | body | string | no |  | Numeric ID of the other person (up to 20 digits) for a one-to-one chat. Used when conversation_id is not set. |
| `text` | body | string | yes |  | Message text, up to 10,000 characters. Must not be empty or only spaces. |

Body parameters are sent as a JSON object with `Content-Type: application/json`.

## Response

{conversation_id, id (sequence ID), message_id, created_at (Unix milliseconds), text}.

With `format=simple`: {data: {id, messageId, conversationId, createdAt, text}}. createdAt is RFC 3339.

## Errors

| Status | When |
| --- | --- |
| 400 | The body is not valid JSON. |
| 422 | cookie or proxy is missing, the cookie has no twid or ct0, or the passcode is neither a string nor a whole number or longer than 64 characters. The passcode or text is missing, text is longer than 10,000 characters, neither conversation_id nor user_id is set, or one of them has the wrong format. Also: the account is suspended, or X rejected the session ("Twitter temporarily rejected this session; refresh the login and try again"). |
| 407 | The proxy is unreachable or timed out. |
| 422 | The recipient cannot receive encrypted messages (no X Chat key), or the conversation has no key you can use (for example a group you cannot write to). |
| 422 | Wrong passcode: "Wrong passcode (N attempts left before X deletes the chat keys)". X deletes the chat keys after too many wrong passcodes, so the API never sends the same wrong passcode to X twice (for 24 hours it answers 422 at once), and once only 3 attempts are left it does not try at all: "Only a few passcode attempts are left for this account; unlock Chat in the X app with the correct passcode first". |
| 422 | The account has no chat passcode yet: open Chat on x.com once and set one up. |
| 501 | Encrypted messages are not available on this server (the passcode cannot be used). Refunded. |
| 502 | The chat keys could not be unlocked ("Could not unlock the chat keys, try again") or the messages could not be decrypted. Refunded. |
| 502 | The message or a new conversation key could not be encrypted. Refunded. |
| 429 | X's rate limit for this account: "X rate limit reached for this account, try again later". |
| X's status | X refused the request: answered with X's status and {"error": "X refused the request: <X's reason>"}. When X answers 200 with an error, the status is 422. |

Shared errors (401 missing key, 402 no balance, 429 rate limit, 5xx) are listed on https://www.twittrapi.com/docs/helpful-infos/error-handling. Calls that fail with 400, 408, 409 or a 5xx error are refunded.

## Example request

curl:

```bash
curl -X POST 'https://api.twittrapi.com/client/dm/send' \
  --header 'X-API-KEY: YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{"cookie":"auth_token=...; ct0=...; twid=u%3D...","proxy":"http://user:pass@host:port","passcode":"1234","user_id":"12","text":"Hello"}'
```

Python:

```python
import httpx

resp = httpx.post(
    "https://api.twittrapi.com/client/dm/send",
    headers={"X-API-KEY": "YOUR_API_KEY"},
    json={
        "cookie": "auth_token=...; ct0=...; twid=u%3D...",
        "proxy": "http://user:pass@host:port",
        "passcode": "1234",
        "user_id": "12",
        "text": "Hello",
    },
    timeout=60,
)
print(resp.status_code, resp.json())
```

Node.js:

```js
const resp = await fetch("https://api.twittrapi.com/client/dm/send", {
  method: "POST",
  headers: { "X-API-KEY": process.env.TWITTRAPI_API_KEY, "Content-Type": "application/json" },
  body: JSON.stringify({
    cookie: "auth_token=...; ct0=...; twid=u%3D...",
    proxy: "http://user:pass@host:port",
    passcode: "1234",
    user_id: "12",
    text: "Hello",
  }),
});
console.log(resp.status, await resp.json());
```
