# Publish to X

> Publish an X post or thread, with text, media, or both.

Canonical: <https://www.tryadeli.com/docs/api/posts/publish-x>

`POST https://app.tryadeli.com/api/v1/posts`

Authorization: `Bearer <API key>` — see [Authentication](https://www.tryadeli.com/docs/authentication)

An X post is text, media, or both. Send `thread` to publish several posts, each
a reply to the one before. X publishes synchronously, so the response is the
finished post.

> **Warning: Every X post is billed**
>
> X charges per API call, and Adeli passes the charge on at X's price: $0.015 a
> post, or $0.20 for a post containing a link. See [X API pricing](https://www.tryadeli.com/docs/pricing/x). Until
> the workspace has a card on file, this returns `402 x_billing_required`.

**Body**

| Name        | Type       | Required | Description                                                                                                                                                                                             |
| ----------- | ---------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `platform`  | `"x"`      | Yes      |                                                                                                                                                                                                         |
| `accountId` | `uuid`     | Yes      | A connected X account in your organization.                                                                                                                                                             |
| `profileId` | `uuid`     |          | Optional, because accountId names its profile. If you pass both, they must agree.                                                                                                                       |
| `text`      | `string`   | Yes      | Up to 280 characters, counted as X counts them: every link is 23. Send `""` for a media-only post. Adeli cannot tell which accounts have X Premium, so the 280 limit applies to all.                    |
| `media`     | `object[]` |          | Up to 4 images, or 1 GIF, or 1 video. Each is `{ content_type, base64 }` with `image/jpeg`, `image/png`, `image/webp`, `image/gif`, or `video/mp4`, or a video as `{ url }`, a public HTTPS MP4 or MOV. |
| `thread`    | `object[]` |          | Up to 24 further posts, each `{ text, media }` with the same rules, published in order as replies.                                                                                                      |

`201` when every post is live.

X cannot publish a thread atomically. When a post after the first fails, the
posts before it are already live and stay live: the response is `207` with
`status: "partial"`, the failed post's `error`, and `not_attempted` for the
rest. Publish the remainder yourself as replies to the last `providerId` that
went live. A failure on the first post publishes nothing and is an ordinary
error.

## Request examples

**cURL**

```bash
curl --fail-with-body -X POST "https://app.tryadeli.com/api/v1/posts" \
  -H "Authorization: Bearer $ADELI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "platform": "x",
  "accountId": "00000000-0000-0000-0000-000000000000",
  "text": "Herons fish at dawn. A thread:",
  "thread": [
    {
      "text": "They stand still for minutes at a time."
    }
  ]
}'
```

**JavaScript**

```javascript
const response = await fetch("https://app.tryadeli.com/api/v1/posts", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ADELI_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    platform: "x",
    accountId: "00000000-0000-0000-0000-000000000000",
    text: "Herons fish at dawn. A thread:",
    thread: [
      {
        text: "They stand still for minutes at a time.",
      },
    ],
  }),
});
console.log(await response.json());
```

**Python**

```python
import os

import requests

response = requests.post(
    "https://app.tryadeli.com/api/v1/posts",
    headers={"Authorization": f"Bearer {os.environ['ADELI_API_KEY']}"},
    json={
        "platform": "x",
        "accountId": "00000000-0000-0000-0000-000000000000",
        "text": "Herons fish at dawn. A thread:",
        "thread": [
            {
                "text": "They stand still for minutes at a time.",
            },
        ],
    },
)
print(response.json())
```

## Responses

### 201

```json
{
  "id": "x_post_00000000-0000-0000-0000-000000000000_1840000000000000000",
  "platform": "x",
  "accountId": "00000000-0000-0000-0000-000000000000",
  "profileId": "00000000-0000-4000-8000-000000000001",
  "status": "published",
  "providerId": "1840000000000000000",
  "permalink": "https://x.com/adeli/status/1840000000000000000",
  "posts": [
    { "index": 0, "status": "published", "providerId": "1840000000000000000", "url": "https://x.com/adeli/status/1840000000000000000" },
    { "index": 1, "status": "published", "providerId": "1840000000000000001", "url": "https://x.com/adeli/status/1840000000000000001" }
  ]
}
```

### 207

```json
{
  "status": "partial",
  "posts": [
    { "index": 0, "status": "published", "providerId": "1840000000000000000", "url": "https://x.com/adeli/status/1840000000000000000" },
    { "index": 1, "status": "failed", "error": { "code": "duplicate_post", "message": "X refused a duplicate of a recent post" } },
    { "index": 2, "status": "not_attempted" }
  ]
}
```

### 402

```json
{
  "error": {
    "code": "x_billing_required",
    "message": "X requires billing to be set up for this account"
  }
}
```
