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

# Create tweet

> Post tweets and replies from a connected X account with public image URLs or 1 MP4 video URL, write-status polling, and audit handoff

<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. Put public HTTPS image URLs or one public MP4 URL in `media`; when `POST /x/media` hosts a local file, use the returned `mediaUrl` here, not `mediaId`. Do not send `media_ids` to this endpoint. Store `tweetId` on `200 Success`, or store `writeActionId` and poll when the response is `202 x_write_unconfirmed`.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://xquik.com/api/v1/x/tweets \
    -H "x-api-key: xq_YOUR_KEY_HERE" \
    -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",
      "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 =
    result.status === "pending_confirmation"
      ? {
          status: result.status,
          write_action_id: result.writeActionId,
          account: "elonmusk",
          charged: result.charged,
          charged_credits: result.chargedCredits,
          retryable: result.retryable,
          poll_path: `/api/v1/x/write-actions/${result.writeActionId}`,
        }
      : {
          status: "posted",
          tweet_id: result.tweetId,
          account: "elonmusk",
          charged: result.charged,
          charged_credits: result.chargedCredits,
        };
  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"},
      json={
          "account": "elonmusk",
          "text": "Hello from Xquik!",
      },
  )
  result = response.json()
  response.raise_for_status()
  if result.get("status") == "pending_confirmation":
      post_record = {
          "status": result["status"],
          "write_action_id": result["writeActionId"],
          "account": "elonmusk",
          "charged": result["charged"],
          "charged_credits": result["chargedCredits"],
          "retryable": result["retryable"],
          "poll_path": f"/api/v1/x/write-actions/{result['writeActionId']}",
      }
  else:
      post_record = {
          "status": "posted",
          "tweet_id": result["tweetId"],
          "account": "elonmusk",
          "charged": result["charged"],
          "charged_credits": result["chargedCredits"],
      }
  print(json.dumps(post_record))
  ```

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

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

  type CreateTweetResponse struct {
      TweetID string `json:"tweetId"`
      Status string `json:"status"`
      WriteActionID string `json:"writeActionId"`
      Charged bool `json:"charged"`
      ChargedCredits string `json:"chargedCredits"`
      Retryable bool `json:"retryable"`
  }

  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("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)
      }
      status := "posted"
      postRecord := map[string]any{
          "status": status,
          "tweet_id": result.TweetID,
          "account": "elonmusk",
          "charged": result.Charged,
          "charged_credits": result.ChargedCredits,
      }
      if result.Status == "pending_confirmation" {
          postRecord = map[string]any{
              "status": result.Status,
              "write_action_id": result.WriteActionID,
              "account": "elonmusk",
              "charged": result.Charged,
              "charged_credits": result.ChargedCredits,
              "retryable": result.Retryable,
              "poll_path": "/api/v1/x/write-actions/" + result.WriteActionID,
          }
      }
      encoded, err := json.Marshal(postRecord)
      if err != nil {
          panic(err)
      }
      fmt.Println(string(encoded))
  }
  ```
</CodeGroup>

## Post with public media URLs

Use `media` when your image or MP4 video is already a public HTTPS URL or when [Upload Media](/api-reference/x-write/upload-media) returned `mediaUrl`. Send up to 4 image URLs or exactly 1 MP4 video URL up to 100 MB. Do not 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 "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 "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 "Content-Type: application/json" \
    -d '{
      "account": "brand_account",
      "text": "Launch walkthrough is live.",
      "media": ["https://cdn.example.com/product-demo.mp4"]
    }' | jq
  ```
</CodeGroup>

Store `tweetId`, `charged`, and `chargedCredits` on a `200 Success` response. If the API returns `202 x_write_unconfirmed`, store `writeActionId`, poll [Get Write Action Status](/api-reference/x-write/get-write-action-status), and do not retry-send the same body. Keep `reply_to_tweet_id` and `media` in your downstream record.

## Headers

<ParamField header="x-api-key" type="string" required>
  Your API key. Session cookie authentication is also supported.
</ParamField>

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

## Body

<ParamField body="account" type="string" required>
  X username or account ID identifying which connected X account to post as. The `@` prefix is automatically stripped if included.
</ParamField>

<ParamField body="text" type="string">
  Tweet text content. Maximum 280 characters for standard tweets, or up to 25,000 characters if `is_note_tweet` is `true`. Optional when `media` is provided.
</ParamField>

<ParamField body="reply_to_tweet_id" type="string">
  Tweet ID to reply to. When set, the new tweet is posted as a reply in that tweet's thread.
</ParamField>

<ParamField body="attachment_url" type="string">
  URL to attach to the tweet as a card. Must be a valid HTTP or HTTPS URL.
</ParamField>

<ParamField body="community_id" type="string">
  X Community ID to post the tweet into. The connected account must be a member of the community.
</ParamField>

<ParamField body="is_note_tweet" type="boolean">
  Set to `true` to post a long-form note tweet (up to 25,000 characters). Defaults to `false`.
</ParamField>

<ParamField body="media" type="string[]">
  Array of public media URLs to attach directly. Send up to 4 JPEG, PNG, GIF, WebP, or AVIF image URLs, or exactly 1 MP4 video URL up to 100 MB. Do not mix video with other media. Use [Upload Media](/api-reference/x-write/upload-media) first if you need Xquik to host a local file, then pass the returned `mediaUrl` in `media`. Do not pass uploaded `mediaId` values or `media_ids`. Attached media adds 2 credits per started MB across all files.
</ParamField>

## Response

<Tabs>
  <Tab title="200 Success">
    <ResponseField name="tweetId" type="string">ID of the newly created tweet.</ResponseField>
    <ResponseField name="success" type="boolean">Always `true` on success.</ResponseField>
    <ResponseField name="charged" type="boolean">Whether credits were charged for the confirmed write.</ResponseField>
    <ResponseField name="chargedCredits" type="string">Credits charged for this tweet. Text-only tweets and replies cost 30 credits; attached media adds 2 credits per started MB.</ResponseField>
    <ResponseField name="writeActionId" type="string">Write action ID when the write was logged for later reconciliation.</ResponseField>

    ```json theme={null}
    {
      "tweetId": "1895432178065391234",
      "success": true,
      "charged": true,
      "chargedCredits": "32"
    }
    ```
  </Tab>

  <Tab title="202 Pending Confirmation">
    ```json theme={null}
    {
      "error": "x_write_unconfirmed",
      "status": "pending_confirmation",
      "writeActionId": "42",
      "charged": false,
      "chargedCredits": "0",
      "retryable": false,
      "message": "Action may have completed, but confirmation is still pending."
    }
    ```

    The write was dispatched but final confirmation is still pending. `writeActionId` identifies the action to poll with [Get write action status](/api-reference/x-write/get-write-action-status). `charged` is `false` and `chargedCredits` is `"0"` while confirmation is pending, and `retryable` is `false`; do not retry-send the same tweet blindly.
  </Tab>

  <Tab title="400 Invalid Input">
    ```json theme={null}
    { "error": "invalid_input", "message": "Either text or media is required" }
    ```

    Missing `text` and `media`, invalid `account` format, or other validation failure.
  </Tab>

  <Tab title="400 Unsupported Field">
    ```json theme={null}
    {
      "error": "unsupported_field",
      "message": "media_ids is not supported on POST /x/tweets. Pass media as an array of public image or MP4 video URLs.",
      "retryable": false,
      "charged": false,
      "chargedCredits": "0",
      "details": {
        "action": "create_tweet",
        "suggestion": "Use the media field with public URLs instead of media_ids. Send up to 4 images or exactly 1 MP4 video up to 100 MB."
      }
    }
    ```

    The request body contains a field the endpoint does not accept (for example `media_ids`).
  </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_content_too_long", "message": "..." }
    ```

    X rejected the write. Possible codes: `x_content_too_long`, `x_duplicate_action`, `x_account_suspended`, `x_account_protected`, `x_target_not_found`, `x_account_feature_required`, `x_rejected`. 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 post tweet" }
    ```

    The tweet could not be posted. The connected account may be suspended, rate-limited, or the content may violate platform policies. Try again later.
  </Tab>
</Tabs>

## Store the post handoff

Store a compact record after every tweet or reply so queues, CRMs, support tools, and agents can reconcile the write without scraping the timeline.

<Tabs>
  <Tab title="Posted">
    ```json theme={null}
    {
      "status": "posted",
      "tweet_id": "1895432178065391234",
      "account": "brand_account",
      "reply_to_tweet_id": "1893456789012345678",
      "media": ["https://cdn.example.com/release-card.png"],
      "charged": true,
      "charged_credits": "32",
      "media_credits": "2"
    }
    ```

    Copy `tweetId` from the response into `tweet_id`. Keep `reply_to_tweet_id` when the post is a reply, and store `chargedCredits` as `charged_credits`. Store the public media URLs you sent in `media`. If you uploaded first, store the returned `mediaUrl`, not the upload `mediaId`.
  </Tab>

  <Tab title="Pending Confirmation">
    ```json theme={null}
    {
      "status": "pending_confirmation",
      "write_action_id": "42",
      "account": "brand_account",
      "reply_to_tweet_id": "1893456789012345678",
      "charged": false,
      "charged_credits": "0",
      "retryable": false,
      "poll": "GET /x/write-actions/{id}"
    }
    ```

    Store `writeActionId` as `write_action_id`, poll [Get Write Action Status](/api-reference/x-write/get-write-action-status), and do not retry-send the same body while status is `pending_confirmation`. If polling later returns `status: "success"` with `tweetId`, update the same record to `posted`.
  </Tab>
</Tabs>

<Note>
  **Next steps:** [Upload Media](/api-reference/x-write/upload-media) to get a `mediaUrl` for a local file, [Get Write Action Status](/api-reference/x-write/get-write-action-status) to poll pending confirmations, [Delete Tweet](/api-reference/x-write/delete-tweet) to remove a posted tweet, [Like](/api-reference/x-write/like) or [Retweet](/api-reference/x-write/retweet) to engage with tweets.
</Note>
