> ## 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 post API for tweet & reply automation

> Post tweets and replies from a connected X account with public image URLs or 1 MP4 video URL. Poll the write status. Text-only posts cost 30 credits each.

<Panel>
  <Tabs defaultTabIndex={0} sync={false}>
    <Tab title="200" id="response-x-write-create-tweet-200">
      ```json theme={null}
      {
        "object": "x_write_action",
        "id": "12345",
        "writeActionId": "12345",
        "action": "create_tweet",
        "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-create-tweet-202">
      ```json theme={null}
      {
        "object": "x_write_action",
        "id": "12346",
        "writeActionId": "12346",
        "action": "create_tweet",
        "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-create-tweet-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-create-tweet-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-create-tweet-402">
      ```json theme={null}
      {
        "error": "insufficient_credits",
        "message": "Insufficient credits. Top up or subscribe to continue."
      }
      ```
    </Tab>

    <Tab title="403" id="response-x-write-create-tweet-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-create-tweet-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-create-tweet-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="413" id="response-x-write-create-tweet-413">
      ```json theme={null}
      {
        "error": "media_too_large",
        "message": "This image stays over X's size limit after shrinking. Send a smaller one."
      }
      ```
    </Tab>

    <Tab title="415" id="response-x-write-create-tweet-415">
      ```json theme={null}
      {
        "error": "unsupported_media_type",
        "message": "Xquik couldn't read this image. Send a valid image file."
      }
      ```
    </Tab>

    <Tab title="422" id="response-x-write-create-tweet-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-create-tweet-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-create-tweet-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-create-tweet-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">
  **30 credits text-only** · attached media adds 2 credits per started MB across all files
</Callout>

Create a tweet or reply from one connected X account.
The route accepts text and public media URLs.
Put public HTTPS images or 1 MP4 URL in `media`.
When `POST /x/media` hosts a local file, use its `mediaUrl`.
Never send `mediaId` or `media_ids` to this endpoint.
Send a unique `Idempotency-Key`.
Store the returned write action.
Poll `statusUrl` while `terminal` is `false`.
X's [Create Post guide](https://docs.x.com/x-api/posts/create-post) covers its separate endpoint.
Authenticate with Xquik credentials. Store the write fields listed below.

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

  ```javascript Node.js theme={null}
  const response = await fetch("https://xquik.com/api/v1/x/tweets", {
    method: "POST",
    headers: {
      "x-api-key": "xq_YOUR_KEY_HERE",
      "Idempotency-Key": "tweet-1895432178065391234",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      account: "elonmusk",
      text: "Hello from Xquik!",
    }),
  });
  const result = await response.json();
  if (!response.ok) {
    throw new Error(JSON.stringify(result));
  }
  const postRecord = {
    status: result.status,
    terminal: result.terminal,
    safe_to_retry: result.safeToRetry,
    write_action_id: result.id,
    request_hash: result.request.hash,
    tweet_id: result.result?.id ?? result.tweetId ?? null,
    account: result.account,
    target: result.target,
    charged: result.billing.charged,
    charged_credits: result.billing.chargedCredits,
    poll_path: result.terminal ? null : result.statusUrl,
  };
  process.stdout.write(`${JSON.stringify(postRecord)}\n`);
  ```

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

  response = requests.post(
      "https://xquik.com/api/v1/x/tweets",
      headers={
          "x-api-key": "xq_YOUR_KEY_HERE",
          "Idempotency-Key": "tweet-1895432178065391234",
      },
      json={
          "account": "elonmusk",
          "text": "Hello from Xquik!",
      },
  )
  result = response.json()
  response.raise_for_status()
  post_record = {
      "status": result["status"],
      "terminal": result["terminal"],
      "safe_to_retry": result["safeToRetry"],
      "write_action_id": result["id"],
      "request_hash": result["request"]["hash"],
      "tweet_id": (result.get("result") or {}).get("id") or result.get("tweetId"),
      "account": result["account"],
      "target": result["target"],
      "charged": result["billing"]["charged"],
      "charged_credits": result["billing"]["chargedCredits"],
      "poll_path": None if result["terminal"] else result["statusUrl"],
  }
  print(json.dumps(post_record))
  ```

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

  import (
      "bytes"
      "encoding/json"
      "fmt"
      "io"
      "net/http"
  )

  type CreateTweetResponse struct {
      ID string `json:"id"`
      TweetID string `json:"tweetId"`
      Status string `json:"status"`
      Terminal bool `json:"terminal"`
      SafeToRetry bool `json:"safeToRetry"`
      StatusURL string `json:"statusUrl"`
      Request struct { Hash *string `json:"hash"` } `json:"request"`
      Billing struct {
          Charged bool `json:"charged"`
          ChargedCredits string `json:"chargedCredits"`
      } `json:"billing"`
      Result *struct { ID string `json:"id"` } `json:"result"`
  }

  func main() {
      body, _ := json.Marshal(map[string]interface{}{
          "account": "elonmusk",
          "text":    "Hello from Xquik!",
      })

      req, err := http.NewRequest("POST", "https://xquik.com/api/v1/x/tweets", bytes.NewReader(body))
      if err != nil {
          panic(err)
      }
      req.Header.Set("x-api-key", "xq_YOUR_KEY_HERE")
      req.Header.Set("Idempotency-Key", "tweet-1895432178065391234")
      req.Header.Set("Content-Type", "application/json")

      resp, err := http.DefaultClient.Do(req)
      if err != nil {
          panic(err)
      }
      defer resp.Body.Close()
      if resp.StatusCode >= 400 {
          body, _ := io.ReadAll(resp.Body)
          panic(string(body))
      }

      var result CreateTweetResponse
      if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
          panic(err)
      }
      var tweetID any
      if result.Result != nil {
          tweetID = result.Result.ID
      } else if result.TweetID != "" {
          tweetID = result.TweetID
      }
      var pollPath any
      if !result.Terminal {
          pollPath = result.StatusURL
      }
      postRecord := map[string]any{
          "status": result.Status,
          "terminal": result.Terminal,
          "safe_to_retry": result.SafeToRetry,
          "write_action_id": result.ID,
          "request_hash": result.Request.Hash,
          "tweet_id": tweetID,
          "account": "elonmusk",
          "charged": result.Billing.Charged,
          "charged_credits": result.Billing.ChargedCredits,
          "poll_path": pollPath,
      }
      encoded, err := json.Marshal(postRecord)
      if err != nil {
          panic(err)
      }
      fmt.Println(string(encoded))
  }
  ```
</CodeGroup>

## Choose the Twitter post request

Choose among 6 tweet request formats. Include only the fields required below.

| Post intent | Required fields | Validation before sending |
| - | - | - |
| Standard tweet | `account` and `text` | Keep standard tweet text at 280 characters or fewer. |
| Tweet reply | `account`, `text`, and `reply_to_tweet_id` | Store the parent Tweet ID and confirm the connected account may reply. |
| Community tweet | `account`, `text`, and `community_id` | Check the account can post in that X Community. |
| Image tweet | `account`, optional `text`, and 1 to 4 image URLs in `media` | Use public HTTPS JPEG, PNG, GIF, WebP, or AVIF URLs. |
| Video tweet | `account`, optional `text`, and 1 MP4 URL in `media` | Keep the public MP4 at 100 MB or less. Do not mix video and images. |
| Note tweet | `account`, `text`, and `is_note_tweet: true` | Keep note tweet text at 25,000 characters or fewer. |

Create one `Idempotency-Key` for each posting request.
Reuse that key only when replaying the same network request. A new
tweet, reply, caption, media URL, account, or community requires a new key.

This endpoint starts the write immediately. It does not schedule future
delivery. Let your scheduler call it at the approved time. One request
publishes 1 tweet on X. Create replies with `reply_to_tweet_id`. Create each
thread tweet with a separate approved request.

For `x_target_not_found`, verify the post ID and your connected account's access.
For `x_reply_not_allowed`, choose a post that account can reply to.
After X refuses a reply, Xquik answers the same account and post with
`x_reply_not_allowed` for 1 hour, without asking X. `Retry-After` says when
to try again.
For `x_reply_target_unavailable`, the post you're replying to is unavailable on X.
Check the post on x.com or reply to another post.
All 3 return HTTP 422 without sending or charging. Do not retry the unchanged request.
See [write error recovery](/guides/error-handling).

## Store the tweet write receipt

Store the write receipt before another worker posts. Keep these field groups:

* Store `id` and `request.hash` for request matching.
* Store `account` and `target` for the selected account and destination.
* Store `status`, `terminal`, and `statusUrl` for polling.
* Store `safeToRetry` for retry decisions.
* Store `result.result?.id` or `tweetId` for the published Tweet ID.
* Store `billing.chargedCredits` for billing checks.

## Post with public media URLs

Use `media` for an image or MP4 at a public HTTPS URL.
For local files, call [Upload Media](/api-reference/x-write/upload-media) first.
Pass its returned `mediaUrl` in `media`.
Send up to 4 image URLs or exactly 1 MP4 URL.
Keep the MP4 at 100 MB or less.
Xquik shrinks images over 5 MiB. GIFs and videos go to X unchanged.
Never send `media_ids`. That field is for DMs only.
Attached media adds 2 credits per started MB across all files.

<CodeGroup>
  ```bash Image tweet theme={null}
  curl -X POST https://xquik.com/api/v1/x/tweets \
    -H "x-api-key: xq_YOUR_KEY_HERE" \
    -H "Idempotency-Key: image-tweet-1895432178065391234" \
    -H "Content-Type: application/json" \
    -d '{
      "account": "brand_account",
      "text": "Launch notes are live.",
      "media": ["https://cdn.example.com/product-screenshot.png"]
    }' | jq
  ```

  ```bash Image reply theme={null}
  curl -X POST https://xquik.com/api/v1/x/tweets \
    -H "x-api-key: xq_YOUR_KEY_HERE" \
    -H "Idempotency-Key: image-reply-1895432178065391234" \
    -H "Content-Type: application/json" \
    -d '{
      "account": "brand_account",
      "text": "Here is the chart.",
      "reply_to_tweet_id": "1893456789012345678",
      "media": ["https://cdn.example.com/reply-chart.png"]
    }' | jq
  ```

  ```bash MP4 video tweet theme={null}
  curl -X POST https://xquik.com/api/v1/x/tweets \
    -H "x-api-key: xq_YOUR_KEY_HERE" \
    -H "Idempotency-Key: video-tweet-1895432178065391234" \
    -H "Content-Type: application/json" \
    -d '{
      "account": "brand_account",
      "text": "Launch walkthrough is live.",
      "media": ["https://cdn.example.com/product-demo.mp4"]
    }' | jq
  ```
</CodeGroup>

Store `id`, `request.hash`, `billing`, `result`, `reply_to_tweet_id`, and `media`. Poll [Get Write Action Status](/api-reference/x-write/get-write-action-status) while `terminal` is `false`. Retry only when `safeToRetry` is `true`, using a new key.

## Headers

<ParamField header="x-api-key" type="string">
  Send your Xquik API key in this header. Alternatively, send an OAuth 2.1 bearer token.
</ParamField>

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

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

## Body

<ParamField body="account" type="string" required>
  Choose the connected X account by username or account ID. Xquik removes an optional `@` prefix.
</ParamField>

<ParamField body="text" type="string">
  Send up to 280 characters for a standard tweet. Note tweets accept up to 25,000 characters. Omit text only when `media` exists.
</ParamField>

<ParamField body="reply_to_tweet_id" type="string">
  Set the parent Tweet ID. Xquik posts the new tweet inside that thread.
</ParamField>

<ParamField body="community_id" type="string">
  Set the target X Community ID. Confirm that the connected account is a member.
</ParamField>

<ParamField body="is_note_tweet" type="boolean">
  Set `true` for a note tweet with up to 25,000 characters. The default is `false`.
</ParamField>

<ParamField body="media" type="string[]">
  Attach public media URLs directly.

  * Send up to 4 JPEG, PNG, GIF, WebP, or AVIF image URLs.
  * Send exactly 1 public MP4 URL up to 100 MB.
  * Never mix video with other media.
  * Use [Upload Media](/api-reference/x-write/upload-media) to host a local file.
  * Pass its returned `mediaUrl` in `media`.
  * Never pass uploaded `mediaId` values or `media_ids`.
  * Attached media adds 2 credits per started MB across all files.
</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.