# Validation and dry runs

Check text length, media and a full post against every platform's rules before you create it, and ask whether a TikTok account can publish right now.

A post that breaks a platform rule fails at publish time, often hours after it was written. The validation endpoints run the same checks up front, with no side effects: nothing is saved, no queue slot is taken and nothing is billed. They do count toward your [rate limit](https://pozy.ai/docs/getting-started/rate-limits), so debounce a live counter. Use them to show problems in your editor while the person is still typing.

| Endpoint | Checks | Use it for |
| --- | --- | --- |
| `POST /v1/tools/validate/post-length` | Character count per platform | A live counter in your editor |
| `POST /v1/tools/validate/post` | The full post: text, media, required fields, per-platform rules | A final check before Schedule |
| `POST /v1/tools/validate/media` | That a URL is reachable, its type and size, and each platform's size limit | Checking a file someone pasted as a link |
| `POST /v1/posts` with `dryRun: true` | Whether each TikTok account can publish right now | Avoiding TikTok daily posting limits |

## Validate a whole post

`POST /v1/tools/validate/post` takes the same body as `POST /v1/posts`. Add `accountId` to an X entry so the check uses that account's real text limit:

```bash cURL
curl -X POST https://api.pozy.ai/v1/tools/validate/post \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Our autumn collection is here. Twelve new pieces, all made in Porto.",
    "platforms": [
      { "platform": "instagram", "accountId": "6710c3e2a9f14b0d8e5c7a22" },
      { "platform": "twitter", "accountId": "6710c3e2a9f14b0d8e5c7a23" }
    ]
  }'
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/tools/validate/post", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.POZY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    content: "Our autumn collection is here. Twelve new pieces, all made in Porto.",
    platforms: [
      { platform: "instagram", accountId: "6710c3e2a9f14b0d8e5c7a22" },
      { platform: "twitter", accountId: "6710c3e2a9f14b0d8e5c7a23" },
    ],
  }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);

if (!data.valid) for (const e of data.errors) console.log(`${e.platform}: ${e.error}`);
```
```python Python
resp = requests.post(
    "https://api.pozy.ai/v1/tools/validate/post",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    json={
        "content": "Our autumn collection is here. Twelve new pieces, all made in Porto.",
        "platforms": [
            {"platform": "instagram", "accountId": "6710c3e2a9f14b0d8e5c7a22"},
            {"platform": "twitter", "accountId": "6710c3e2a9f14b0d8e5c7a23"},
        ],
    },
    timeout=30,
)
result = resp.json()
for e in result.get("errors", []):
    print(e["platform"], e["error"])
```

```json
{
  "valid": false,
  "errors": [
    { "platform": "instagram", "error": "Instagram posts need at least one image or video." }
  ],
  "warnings": []
}
```

When everything passes, `valid` is `true` and `message` confirms it. `warnings` lists things that will publish but deserve a look, such as text above 90% of a platform's limit.

What it does not do: it does not check that the account is connected and healthy, and it does not download or process media. For accounts use [account health](https://pozy.ai/docs/connect/account-health); for files use `validate/media` below.

## Count characters per platform

`POST /v1/tools/validate/post-length` takes only `text` and returns a count, limit and verdict per platform:

```json
{
  "text": "Our autumn collection is here. Twelve new pieces, all made in Porto.",
  "platforms": {
    "twitter": { "count": 69, "limit": 280, "valid": true },
    "twitterPremium": { "count": 69, "limit": 25000, "valid": true },
    "bluesky": { "count": 69, "limit": 300, "valid": true },
    "instagram": { "count": 69, "limit": 2200, "valid": true }
  }
}
```

X counts differently from the others: every link counts as 23 characters and most emoji count as 2. Two X rows come back because the limit depends on the account. `twitter` is the standard 280. `twitterPremium` applies only to accounts with a paid X subscription, and even those are held to 280 when the post has a poll. Since this endpoint never sees an account, trust `validate/post` with an `accountId` for the final answer.

Main text limits:

| Platform | Limit |
| --- | --- |
| X | 280 (more with a paid X subscription) |
| Threads | 500 |
| Bluesky | 300 |
| LinkedIn | 3,000 |
| Instagram | 2,200 |
| TikTok | 2,200 in the caption |
| YouTube | 5,000 in the description, 100 in the title |
| Pinterest | 500 in the description, 100 in the title |
| Google Business Profile | 1,500 |
| Facebook | 63,206 |
| Telegram | 4,096 |
| Discord | 2,000 |

Per-platform pages list other limits, such as hashtag counts and title lengths.

## Check a media URL

`POST /v1/tools/validate/media` fetches the headers of a URL and tells you what it found:

```json
{ "url": "https://example.com/files/autumn-lookbook.mp4" }
```

The response has `valid`, `contentType`, `size` (with a readable `sizeFormatted`), `type` (`image`, `video` or `unknown`), and `platformLimits` comparing the file with each platform's maximum size. A URL that is unreachable, private or returns a web page comes back with `valid: false` and an `error` you can show. Local and private network addresses are always refused.

## Ask TikTok before you post

TikTok limits how many posts some accounts can publish in a rolling 24 hours. To check before committing, send the normal create request with `dryRun: true`. Nothing is saved and no queue slot is claimed, so you can repeat it freely:

```json
{
  "dryRun": true,
  "canPublish": false,
  "tiktok": [
    {
      "accountId": "6710c3e2a9f14b0d8e5c7a24",
      "canPublish": false,
      "alreadyPostedToday": false,
      "reason": "Daily active user quota reached"
    }
  ]
}
```

When `canPublish` is `false`, schedule the post a few hours out, or send it as a TikTok draft with `tiktokSettings.draft: true`, which lands in the creator's TikTok inbox and is not limited in the same way. See [TikTok](https://pozy.ai/docs/platforms/tiktok).

## Validate CSV files

For a spreadsheet of posts, `POST /v1/posts/bulk-upload?dryRun=true` validates every row at once. See [CSV import](https://pozy.ai/docs/publishing/csv-import).

`GET /v1/tools/validate/subreddit` is paused along with Reddit publishing and returns `503` until Reddit reopens its API.

## Endpoints

| Endpoint | What it does |
| --- | --- |
| [`POST /v1/tools/validate/post-length`](https://pozy.ai/docs/api/validatePostLength) | Validate character count |
| [`POST /v1/tools/validate/post`](https://pozy.ai/docs/api/validatePost) | Validate post content |
| [`POST /v1/tools/validate/media`](https://pozy.ai/docs/api/validateMedia) | Validate media URL |
| [`GET /v1/tools/validate/subreddit`](https://pozy.ai/docs/api/validateSubreddit) | Check subreddit existence |

| Endpoint | What it does |
| --- | --- |
| [`POST /v1/posts`](https://pozy.ai/docs/api/createPost) | Create post |
| [`POST /v1/posts/bulk-upload`](https://pozy.ai/docs/api/bulkUploadPosts) | Bulk upload from CSV |
