> ## 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.

# Twitter DM API: send direct messages with media

> Send text or one media attachment as a direct message from a connected X account. Poll the write action and handle every response. Each send costs 10 credits.

<Panel>
  <Tabs defaultTabIndex={0} sync={false}>
    <Tab title="200" id="response-x-write-send-dm-200">
      ```json theme={null}
      {
        "object": "x_write_action",
        "id": "12345",
        "writeActionId": "12345",
        "action": "send_dm",
        "status": "success",
        "terminal": true,
        "retryable": false,
        "safeToRetry": false,
        "statusUrl": "/api/v1/x/write-actions/12345",
        "pollAfterMs": null
      }
      ```
    </Tab>

    <Tab title="202" id="response-x-write-send-dm-202">
      ```json theme={null}
      {
        "object": "x_write_action",
        "id": "12346",
        "writeActionId": "12346",
        "action": "send_dm",
        "status": "dispatching",
        "terminal": false,
        "retryable": false,
        "safeToRetry": false,
        "statusUrl": "/api/v1/x/write-actions/12346",
        "pollAfterMs": 2000
      }
      ```
    </Tab>

    <Tab title="400" id="response-x-write-send-dm-400">
      ```json theme={null}
      {
        "error": "missing_idempotency_key",
        "message": "Idempotency-Key is required. Generate one unique key for this write.",
        "charged": false,
        "chargedCredits": "0",
        "retryable": false,
        "safeToRetry": true
      }
      ```
    </Tab>

    <Tab title="401" id="response-x-write-send-dm-401">
      ```json theme={null}
      {
        "error": "unauthenticated",
        "message": "Authentication required. Provide a valid API key or bearer token."
      }
      ```
    </Tab>

    <Tab title="402" id="response-x-write-send-dm-402">
      ```json theme={null}
      {
        "error": "insufficient_credits",
        "message": "Insufficient credits. Top up or subscribe to continue."
      }
      ```
    </Tab>

    <Tab title="403" id="response-x-write-send-dm-403">
      ```json theme={null}
      {
        "error": "account_needs_reauth",
        "message": "X account needs re-authentication. Re-add the account."
      }
      ```
    </Tab>

    <Tab title="404" id="response-x-write-send-dm-404">
      ```json theme={null}
      {
        "error": "account_not_found",
        "message": "X account not found. Connect it first at /dashboard/account?tab=x-accounts."
      }
      ```
    </Tab>

    <Tab title="409" id="response-x-write-send-dm-409">
      ```json theme={null}
      {
        "error": "idempotency_conflict",
        "message": "Idempotency-Key was already used with a different request.",
        "charged": false,
        "chargedCredits": "0",
        "retryable": false,
        "safeToRetry": true
      }
      ```
    </Tab>

    <Tab title="422" id="response-x-write-send-dm-422">
      ```json theme={null}
      {
        "error": "x_rejected",
        "message": "X rejected this request. Check what you sent & the account on x.com before you try again."
      }
      ```
    </Tab>

    <Tab title="429" id="response-x-write-send-dm-429">
      ```json theme={null}
      {
        "error": "rate_limit_exceeded",
        "message": "Too many requests. Try again later.",
        "retryAfter": 60
      }
      ```
    </Tab>

    <Tab title="500" id="response-x-write-send-dm-500">
      ```json theme={null}
      {
        "error": "x_write_failed",
        "message": "Write action failed unexpectedly. Contact support if this persists."
      }
      ```
    </Tab>

    <Tab title="503" id="response-x-write-send-dm-503">
      ```json theme={null}
      {
        "error": "write_tracking_unavailable",
        "message": "Write tracking unavailable. Try again.",
        "charged": false,
        "chargedCredits": "0",
        "retryable": true,
        "safeToRetry": true
      }
      ```
    </Tab>
  </Tabs>
</Panel>

<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 send** · [Compare plans](https://xquik.com/#pricing)
</Callout>

## Send direct messages with the Twitter DM API

Use this route for approved, one-to-one messages.
It sends text or one uploaded media attachment.
SDKs, support tools, and server jobs can call it.

Choose the connected X account and the recipient's numeric user ID.
Use [Twitter profile lookup](/api-reference/x/twitter-profile-lookup) when you only know a username.
Follow [X's Direct Message rules](https://docs.x.com/x-api/direct-messages/manage/integrate) for every message.

<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 "Idempotency-Key: dm-44196397-1895432178065391234" \
    -H "Content-Type: application/json" \
    -d '{
      "account": "myxaccount",
      "text": "Your requested account update is ready."
    }' | jq
  ```

  ```javascript Node.js theme={null}
  const recipientUserId = "44196397";
  const dmResponse = await fetch(`https://xquik.com/api/v1/x/dm/${recipientUserId}`, {
    method: "POST",
    headers: {
      "x-api-key": "xq_YOUR_KEY_HERE",
      "Idempotency-Key": "dm-44196397-1895432178065391234",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      account: "myxaccount",
      text: "Your requested account update is ready.",
    }),
  });

  const dmAction = await dmResponse.json();
  if (!dmResponse.ok) {
    throw new Error(dmAction.message);
  }

  process.stdout.write(`${JSON.stringify(dmAction)}\n`);
  ```
</CodeGroup>

## Authenticate and select the recipient

Authenticate with an `x-api-key` header or an OAuth bearer token.
The connected X account sends the message on the user's behalf.
The path `userId` identifies the recipient, not the sender.

Send only messages approved by your workflow.
Honor recipient privacy, consent, and opt-out decisions.
Read [DM history](/api-reference/x/dm-history) only for approved conversation context.

## Send text or one media attachment

Provide non-empty `text` for every direct message.
Upload media first with [Upload Media](/api-reference/x-write/upload-media).
Place its `mediaId` inside a one-item `media_ids` array.
The upload costs another 10 credits.

Empty or multi-item arrays return `400 invalid_input`.
This route also rejects `reply_to_message_id`.
It does not send group messages or accept public media URLs.

## Poll and verify the direct message

A `200` response contains a terminal write action.
A `202` response requires polling through `statusUrl`.
Wait for `terminal: true` before closing the send job.

Xquik settles a pending DM within 15 minutes of the send:

* `success` with `messageId` means X sent it. You pay once.
* `failed` with `x_dm_not_sent` means the conversation shows no DM from the sender since shortly before the send. You pay nothing, and `safeToRetry` is `true`. If you deleted the DM on X yourself, check before you send it again.
* `expired` means Xquik could not check the conversation. You pay nothing. Check the conversation before you send again.

Store `writeActionId`, recipient ID, account, request hash, and `messageId`.
[Delete DM](/api-reference/x-write/delete-dm) takes that `messageId` later.

If the connection drops, retry with the same idempotency key.
Never create another write while the first action remains nonterminal.
The Xquik POST limit is 120 requests per minute.

## Handle every Twitter DM API response

Fix request fields after `400`.
Replace authentication after `401`, then add credits after `402`.
Reconnect the X account after `403`.
Keep the original request after a `409` idempotency conflict.

`422 x_dm_not_allowed` means this sender cannot message that person.
Try another approved sender or ask the person to allow DMs.
Honor `Retry-After` after `429`.
`429 x_rate_limited` means X paused DMs from that account.
Its DMs then get `429` until X's reset, and Xquik sends nothing to X.
You pay nothing. `Retry-After` and `nextAction.afterMs` give the wait.
After `500` or `503`, inspect `safeToRetry` before retrying.

## Twitter DM API questions

### Can I automate customer support direct messages?

Yes. Queue approved replies and store the exact sent text.
Avoid unsolicited bulk messaging and honor every opt-out.

### Can I send images or video through the API?

Upload the file first, then send its single `mediaId`.
This endpoint accepts one attachment per message.

### Does this endpoint retrieve message history or create webhooks?

No. Use [Get DM History](/api-reference/x/dm-history) for prior messages.
This send route does not configure incoming-DM notifications.

### Can third-party tools and generated SDKs call this route?

Yes. Use the documented REST fields and authentication.
Leave `reply_to_message_id` unset when a generated SDK exposes it.

## Headers

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

<ParamField header="Idempotency-Key" type="string" required>
  Unique key for this intended send. Reuse it only for an exact network replay.
</ParamField>

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

## Path parameters

<ParamField path="userId" type="string" required>
  DM recipient: user ID, username with or without `@`, or URL-encoded profile URL, such as `x.com/nasa`.
  An unknown username returns `422 x_target_not_found`. See [path IDs](/api-reference/overview#path-ids).
</ParamField>

## Body

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

<ParamField body="text" type="string" required>
  Non-empty direct message text.
</ParamField>

<ParamField body="media_ids" type="string[]">
  Optional one-item array containing an uploaded media ID.
</ParamField>

## Response

## Durable write recovery

<Warning>
  Send one unique `Idempotency-Key` per intended write.
  Replay the same account, target, payload, and media after a lost response.
  Keep the original key for that replay.
</Warning>

1. Store `id`, the nested `hash` in `request`, `billing`, and `statusUrl`.
2. Poll after `Retry-After` or `pollAfterMs` when `terminal` is `false`.
3. Retry only when `safeToRetry` is `true`.
4. Use a new key when `nextAction.requiresNewIdempotencyKey` is `true`.

### 200 terminal or 202 active

* After HTTP `200`, store the result and settled billing.
* After HTTP `202`, poll the same action. Never submit another write.
* After HTTP `400`, fix the named field. Use a new idempotency key.
* After HTTP `401`, fix authentication. Do not retry unchanged.
* After HTTP `402`, fund the account before another write.
* After HTTP `403`, reconnect the account.
* After HTTP `409`, keep the original action. Use a new key for new input.
* After HTTP `422`, fix the rejected request before retrying.
* After HTTP `429`, wait for `Retry-After`. Follow `nextAction`.

See [Get Write Action Status](/api-reference/x-write/get-write-action-status)
for every lifecycle field, terminal state, billing field, and retry rule.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.