> ## Documentation Index
> Fetch the complete documentation index at: https://docs.xquik.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Send DM

> Send X direct messages from a connected account, store messageId, and attach one uploaded media ID

<blockquote className="agent-llms-directive">
  For the complete documentation index, see <a href="/llms.txt">llms.txt</a>.
</blockquote>

<Callout icon="coins" color="#5c3327">
  **10 credits per call** · [All plans](https://xquik.com/#pricing) from \$0.00012/credit
</Callout>

Send a text DM from one connected X account to the recipient `userId`. For media DMs, upload first with `POST /x/media`, pass the returned `mediaId` as the only `media_ids` item, and store the returned `messageId`. Use public media URLs with `POST /x/tweets`; this DM endpoint accepts one uploaded media ID instead.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://xquik.com/api/v1/x/dm/44196397 \
    -H "x-api-key: xq_YOUR_KEY_HERE" \
    -H "Content-Type: application/json" \
    -d '{
      "account": "myxaccount",
      "text": "Hello from Xquik!"
    }' | jq
  ```

  ```javascript Node.js theme={null}
  const recipientUserId = "44196397";
  const account = "myxaccount";
  const uploadedMediaId = "1893726451023847424";
  const mediaIds = [uploadedMediaId];

  const response = await fetch("https://xquik.com/api/v1/x/dm/44196397", {
    method: "POST",
    headers: {
      "x-api-key": "xq_YOUR_KEY_HERE",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      account,
      text: "Hello from Xquik!",
      media_ids: mediaIds,
    }),
  });
  const result = await response.json();

  const dmHandoff = {
    message_id: result.messageId,
    user_id: recipientUserId,
    account,
    send_status: result.success ? "sent" : "unknown",
    media_ids: mediaIds,
    media_id: uploadedMediaId,
    source_endpoint: `/api/v1/x/dm/${recipientUserId}`,
  };

  process.stdout.write(`${JSON.stringify(dmHandoff)}\n`);
  ```

  ```python Python theme={null}
  import json
  import requests

  recipient_user_id = "44196397"
  account = "myxaccount"
  uploaded_media_id = "1893726451023847424"
  media_ids = [uploaded_media_id]

  response = requests.post(
      f"https://xquik.com/api/v1/x/dm/{recipient_user_id}",
      headers={"x-api-key": "xq_YOUR_KEY_HERE"},
      json={
          "account": account,
          "text": "Hello from Xquik!",
          "media_ids": media_ids,
      },
  )
  result = response.json()

  dm_handoff = {
      "message_id": result["messageId"],
      "user_id": recipient_user_id,
      "account": account,
      "send_status": "sent" if result["success"] else "unknown",
      "media_ids": media_ids,
      "media_id": uploaded_media_id,
      "source_endpoint": f"/api/v1/x/dm/{recipient_user_id}",
  }

  print(json.dumps(dm_handoff))
  ```

  ```go Go theme={null}
  package main

  import (
      "bytes"
      "encoding/json"
      "log"
      "net/http"
      "os"
  )

  type SendDmRequest struct {
      Account  string   `json:"account"`
      Text     string   `json:"text"`
      MediaIDs []string `json:"media_ids,omitempty"`
  }

  type SendDmResult struct {
      MessageID string `json:"messageId"`
      Success   bool   `json:"success"`
  }

  type DmHandoff struct {
      MessageID      string  `json:"message_id"`
      UserID         string  `json:"user_id"`
      Account        string  `json:"account"`
      SendStatus     string  `json:"send_status"`
      MediaIDs       []string `json:"media_ids"`
      MediaID        string  `json:"media_id"`
      SourceEndpoint string  `json:"source_endpoint"`
  }

  func sendStatus(success bool) string {
      if success {
          return "sent"
      }
      return "unknown"
  }

  func main() {
      recipientUserID := "44196397"
      account := "myxaccount"
      uploadedMediaID := "1893726451023847424"
      uploadedMediaIDs := []string{uploadedMediaID}

      body, err := json.Marshal(SendDmRequest{
          Account:  account,
          Text:     "Hello from Xquik!",
          MediaIDs: uploadedMediaIDs,
      })
      if err != nil {
          log.Fatal(err)
      }

      req, err := http.NewRequest(
          "POST",
          "https://xquik.com/api/v1/x/dm/"+recipientUserID,
          bytes.NewReader(body),
      )
      if err != nil {
          log.Fatal(err)
      }
      req.Header.Set("x-api-key", "xq_YOUR_KEY_HERE")
      req.Header.Set("Content-Type", "application/json")

      resp, err := http.DefaultClient.Do(req)
      if err != nil {
          log.Fatal(err)
      }
      defer resp.Body.Close()

      var result SendDmResult
      if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
          log.Fatal(err)
      }

      handoff := DmHandoff{
          MessageID:      result.MessageID,
          UserID:         recipientUserID,
          Account:        account,
          SendStatus:     sendStatus(result.Success),
          MediaIDs:       uploadedMediaIDs,
          MediaID:        uploadedMediaID,
          SourceEndpoint: "/api/v1/x/dm/" + recipientUserID,
      }

      if err := json.NewEncoder(os.Stdout).Encode(handoff); err != nil {
          log.Fatal(err)
      }
  }
  ```
</CodeGroup>

The Node.js, Python, and Go examples convert the response into one DM send row.
Store `message_id`, `user_id`, `account`, `send_status`, optional `media_id`,
`media_ids`, and `source_endpoint`. For media DMs, keep `media_ids` as the
one-item request array and set `media_id` to the uploaded media ID that you
passed as that single item.

## Send with media

Upload media first with [Upload Media](/api-reference/x-write/upload-media), then pass the returned `mediaId` as the only `media_ids` item.

```bash theme={null}
curl -X POST https://xquik.com/api/v1/x/dm/44196397 \
  -H "x-api-key: xq_YOUR_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "account": "myxaccount",
    "text": "Here is the requested image.",
    "media_ids": ["1893726451023847424"]
  }' | jq
```

<Tip>
  `media_ids` must contain exactly one uploaded media ID. Empty arrays, multiple IDs, and `reply_to_message_id` return `400 invalid_input`.
</Tip>

Generated SDKs can expose `reply_to_message_id` while the REST route rejects it.
Leave it unset. Pass exactly one uploaded media ID in `media_ids`, then store
the returned `messageId` with that media ID.

### Media DM result handoff

After the media DM send returns `200 OK`, store the returned `messageId` with the uploaded media ID and recipient user ID so support logs, CRM records, queues, or agent memory can reconcile the attachment with the sent message.

```json theme={null}
{
  "record_type": "dm_media_send",
  "message_id": "1893726451029384192",
  "user_id": "44196397",
  "account": "myxaccount",
  "media_ids": ["1893726451023847424"],
  "media_id": "1893726451023847424",
  "send_status": "sent",
  "handoff_format": "jsonl"
}
```

Keep `media_ids` as a one-item array in the request, keep `media_id` as the original `POST /x/media` result, and use `message_id` as the external DM identifier after the send succeeds.

## Direct message handoff

Use `POST /x/dm/{userId}` when a support, sales, community, CRM, or agent workflow needs to send an auditable direct message from a connected X account. If you only have a username, look up the recipient first with [`GET /x/users/{id}`](/api-reference/x/get-user). If the workflow needs conversation context, read the latest messages with [`GET /x/dm/{userId}/history`](/api-reference/x/dm-history) before sending.

<CardGroup cols={2}>
  <Card title="messageId" icon="mail-check">
    Store `messageId` as the external message ID for support logs, CRM records, queues, or agent memory.
  </Card>

  <Card title="success" icon="circle-check">
    Mark the send job complete after a `200 OK` response.
  </Card>

  <Card title="userId" icon="user">
    Keep the recipient X user ID from the path with the send job.
  </Card>

  <Card title="account" icon="at-sign">
    Store the connected X account that sent the DM.
  </Card>

  <Card title="text" icon="message-square">
    Store the exact message text sent. Add your own `sent_at` timestamp when downstream systems need it.
  </Card>

  <Card title="media_ids[0]" icon="paperclip">
    Store the uploaded media ID when the DM includes one attachment from [`POST /x/media`](/api-reference/x-write/upload-media).
  </Card>
</CardGroup>

This endpoint costs 10 credits per send. Uploading media first with `POST /x/media` is a separate 10-credit call. Do not retry `422 x_dm_not_allowed` unchanged; use another permitted connected account or ask the recipient to allow messages.

## Headers

<ParamField header="x-api-key" type="string" required>
  Your API key. Session cookie authentication is also supported. Generate a key from the [dashboard](https://xquik.com/dashboard).
</ParamField>

<ParamField header="Content-Type" type="string" required>
  Must be `application/json`.
</ParamField>

## Path parameters

<ParamField path="userId" type="string" required>
  The X user ID of the recipient.
</ParamField>

## Body

<ParamField body="account" type="string" required>
  X username or account ID of your connected account to act as.
</ParamField>

<ParamField body="text" type="string" required>
  Non-empty message text to send. If X rejects oversized content, the API
  returns `422 x_content_too_long`.
</ParamField>

<ParamField body="media_ids" type="string[]">
  Optional one-item array containing an uploaded media ID. Upload media first with [Upload Media](/api-reference/x-write/upload-media), then pass the returned `mediaId` as the only array item. Empty arrays and multiple IDs are rejected.
</ParamField>

## Response

<Tabs>
  <Tab title="200 OK">
    <ResponseField name="messageId" type="string">The ID of the sent message.</ResponseField>
    <ResponseField name="success" type="boolean">Always `true` on success.</ResponseField>

    ```json theme={null}
    {
      "messageId": "1893726451029384192",
      "success": true
    }
    ```
  </Tab>

  <Tab title="400 Invalid Input">
    ```json theme={null}
    { "error": "invalid_input", "message": "Missing or invalid text or account" }
    ```

    The request body is missing required fields, includes zero or more than one `media_ids` item, includes an unsupported `reply_to_message_id`, or contains invalid values.
  </Tab>

  <Tab title="401 Unauthenticated">
    ```json theme={null}
    { "error": "unauthenticated", "message": "Missing or invalid API key" }
    ```

    Missing or invalid API key.
  </Tab>

  <Tab title="402 Subscription Required">
    ```json theme={null}
    { "error": "no_subscription", "message": "No active subscription" }
    ```

    An active subscription is required. Subscribe from the [dashboard](https://xquik.com/subscription).
  </Tab>

  <Tab title="402 Insufficient Credits">
    ```json theme={null}
    { "error": "insufficient_credits", "message": "Insufficient credits" }
    ```

    Insufficient credits for this operation. Top up your credit balance from the [dashboard](https://xquik.com/dashboard).
  </Tab>

  <Tab title="403 Account Needs Reauth">
    ```json theme={null}
    { "error": "account_needs_reauth" }
    ```

    The connected X account failed a recent login attempt and needs to be reconnected. Reconnect it from the [dashboard](https://xquik.com/dashboard).
  </Tab>

  <Tab title="403 Account Restricted">
    ```json theme={null}
    { "error": "account_restricted" }
    ```

    The connected X account is locked, suspended, recovering, or temporarily restricted. Resolve the restriction on X before retrying.
  </Tab>

  <Tab title="422 Write Rejected">
    ```json theme={null}
    { "error": "x_dm_not_allowed", "message": "Cannot send a DM to this user. They may not accept messages from this account." }
    ```

    X rejected the write. Possible codes: `x_dm_not_allowed`, `x_content_too_long`, `x_duplicate_action`, `x_account_suspended`, `x_account_protected`, `x_target_not_found`, `x_account_feature_required`, `x_rejected`. If you receive `x_dm_not_allowed`, the recipient may not accept messages from this connected account; do not retry unchanged. See [error handling](/guides/error-handling) for details.
  </Tab>

  <Tab title="429 Rate Limited">
    ```json theme={null}
    { "error": "rate_limit_exceeded", "retryAfter": 60 }
    ```

    The request hit an Xquik tier limit, or X throttled the write. Possible codes: `rate_limit_exceeded`, `x_rate_limited`, or `x_daily_limit`. Respect the `Retry-After` header when present.
  </Tab>

  <Tab title="503 Transient Error">
    ```json theme={null}
    { "error": "x_transient_error", "message": "..." }
    ```

    A transient write service issue occurred. Safe to retry with exponential backoff.
  </Tab>

  <Tab title="500 Write Failed">
    ```json theme={null}
    { "error": "x_write_failed", "message": "Failed to complete the action" }
    ```

    The write action failed. Retry after a short delay.
  </Tab>
</Tabs>

<Note>
  **Related:** [Direct Message Workflow](/guides/direct-message-workflow) for
  lookup, history sync, `messageId` storage, and media handoff; [Get User](/api-reference/x/get-user)
  to look up a user's ID before messaging; [Get DM History](/api-reference/x/dm-history)
  to sync participant-scoped messages; [Upload Media](/api-reference/x-write/upload-media)
  to create the one `mediaId` allowed in `media_ids`.
</Note>
