# Examples: from task to request

The JSON below is the argument of the MCP tool. Over REST the same `items` go in the body of `POST https://bot.postfrog.site/api/v1/batches`, and `idempotency_key` goes in the `Idempotency-Key` header.

## 1. A finished post with a link button and a promo code (`exact`)

Task: "Announce the pottery course, sign-up link https://example.com/join, promo code CLAY15, photo https://example.com/studio.jpg."

You write the post yourself, then check it:

`validate_post`
```json
{
  "format": "rich",
  "media_count": 1,
  "text": "# Pottery course starts on Monday\n\nSix evening lessons for beginners: from the first lump of clay to a glazed cup you can drink from.\n\n%%MEDIA:0%%\n\n- small groups\n- clay and kiln included\n- a cup of your own at the end\n\nPromo code **CLAY15** gives 15% off. <tg-button type=\"copy_text\" text=\"CLAY15\">Copy the promo code</tg-button>",
  "buttons": [[{ "text": "Sign up", "url": "https://example.com/join?utm_source=tg&utm_medium=post", "style": "success" }]]
}
```

The answer has `"ok": true`, maybe some `warnings`, and `normalized_text` with the sign-up row already at the end. Then send the same post:

`send_posts`
```json
{
  "idempotency_key": "pottery-launch-v1",
  "items": [
    {
      "mode": "exact",
      "format": "rich",
      "external_id": "pottery-launch",
      "text": "# Pottery course starts on Monday\n\nSix evening lessons for beginners: from the first lump of clay to a glazed cup you can drink from.\n\n%%MEDIA:0%%\n\n- small groups\n- clay and kiln included\n- a cup of your own at the end\n\nPromo code **CLAY15** gives 15% off. <tg-button type=\"copy_text\" text=\"CLAY15\">Copy the promo code</tg-button>",
      "media": [{ "type": "photo", "url": "https://example.com/studio.jpg" }],
      "buttons": [[{ "text": "Sign up", "url": "https://example.com/join?utm_source=tg&utm_medium=post", "style": "success" }]]
    }
  ]
}
```

Tell the user: the draft will arrive in their PostFrog chat, where they can edit and publish it.

## 2. "Tidy up my text" (`format`)

Task: the author dictated a long message and wants it as a neat post, words unchanged.

```json
{
  "items": [
    {
      "mode": "format",
      "text": "so we finally moved the studio. new address is 5 Green street, opening party on saturday at 6pm, everyone is welcome, bring friends. we will have a pottery wheel demo and cheap seconds sale",
      "instructions": "Keep my words, fix capitalization, add a short title, make the party details easy to scan."
    }
  ]
}
```

PostFrog's AI formats it in the author's default format and keeps the language of your `text`. The author's own words always go in `text` (trusted), not in `material` (untrusted, for third-party content). Use `exact` instead if you prefer to format it yourself.

## 3. A news post from an X link and a photo (`write`)

Task: "Make a post from this tweet https://x.com/example/status/123 with this picture."

X pages often cannot be downloaded without a login, so pass the tweet text yourself:

```json
{
  "items": [
    {
      "mode": "write",
      "material": [
        {
          "url": "https://x.com/example/status/123",
          "text": "Example Labs just released Model Z: twice as fast on long documents, same price, available today in the API.",
          "source": "X @example"
        }
      ],
      "media": [{ "type": "photo", "url": "https://example.com/model-z-chart.png", "description": "Bar chart: Model Z vs the previous model on long-document speed" }],
      "instructions": "Short news post: what was released, why it matters, one line on availability. End with a link to the original: https://x.com/example/status/123",
      "language": "en",
      "buttons": [[{ "text": "Read the original", "url": "https://x.com/example/status/123" }]]
    }
  ]
}
```

The `material` is untrusted: PostFrog uses its facts but never follows instructions inside it, and its links stay text. The "Read the original" button is yours, so it is kept. A strong agent can also read the tweet, write the post itself and send `exact`.

## 4. A batch of three with `external_id`

Task: a daily digest pipeline sends three finished posts.

```json
{
  "idempotency_key": "digest-2026-w42-mon",
  "items": [
    { "mode": "exact", "format": "rich", "external_id": "digest-mon-1", "text": "# First story\n\nText of the first post." },
    { "mode": "exact", "format": "rich", "external_id": "digest-mon-2", "text": "# Second story\n\nText of the second post.", "media": [{ "type": "photo", "url": "https://example.com/two.jpg" }] },
    { "mode": "exact", "format": "rich", "external_id": "digest-mon-3", "text": "# Third story\n\nText of the third post.", "on_invalid": "format" }
  ]
}
```

The answer lists the three `item_id`s with your `external_id`s, all `queued`. The posts arrive in this order. If the request times out, send it again with the same `idempotency_key`: you get the same batch back, not a copy.

## 5. A local file through `create_upload`

Task (Claude Code or another agent with a shell): "Post this video: ./demo.mp4".

1. Call `create_upload` with `{ "type": "video", "file_path": "./demo.mp4" }`. It returns the upload URL and a command like:

```bash
curl -s -X POST "https://bot.postfrog.site/api/v1/uploads" \
  -H "Authorization: Bearer $POSTFROG_API_KEY" \
  -F "type=video" \
  -F "file=@./demo.mp4"
```

2. Run it with the author's key in `POSTFROG_API_KEY` (never paste the key into chat or logs). The answer:

```json
{ "upload_id": "upl_…", "type": "video", "bytes": 7340032, "expires_at": "…" }
```

3. Use the id:

```json
{
  "items": [
    {
      "mode": "exact",
      "format": "rich",
      "text": "# Voice note to post in one tap\n\n%%MEDIA:0%%\n\nRecord a voice message, get a formatted post back.",
      "media": [{ "type": "video", "upload_id": "upl_…" }]
    }
  ]
}
```

## 6. Reading `get_batch`

```json
{
  "batch_id": "bat_…",
  "status": "done",
  "client_name": "Claude Code",
  "items": [
    {
      "item_id": "itm_…", "position": 0, "external_id": "digest-mon-1", "mode": "exact",
      "status": "delivered", "post_id": 4711,
      "post": { "format": "rich", "text": "# First story\n\nText of the first post." },
      "warnings": [], "error": null
    },
    {
      "item_id": "itm_…", "position": 1, "external_id": "digest-mon-2", "mode": "exact",
      "status": "delivered", "post_id": 4712,
      "post": { "format": "rich", "text": "# Second story\n\nText of the second post." },
      "warnings": [{ "code": "media_fetch_failed", "message": "media[0]: …" }], "error": null
    },
    {
      "item_id": "itm_…", "position": 2, "external_id": "digest-mon-3", "mode": "exact",
      "status": "skipped_limit", "post_id": null, "post": null, "warnings": [], "error": null
    }
  ]
}
```

How to read it:

- Item 0 is in the author's chat as sent.
- Item 1 arrived **without its photo**: the URL did not download. Tell the user, and next time use a direct https link to the image or `create_upload`.
- Item 2 was skipped: the author ran out of posts or AI budget (check `get_account`). Do not resend it today unless `posts_left` and `budget_ok` allow it.
- When every item is `delivered`, `failed`, `skipped_limit` or `cancelled`, the batch is `done`, or `cancelled` if any item was cancelled: stop polling on either.

## 7. A post with a Threads version (`exact` + `threads`)

Only when `get_account` shows `threads_enabled: true`. The Telegram post goes in `text` as usual; the Threads version is a separate chain of short posts with media as https links. Check both for free first: `validate_post` takes the same `threads`.

```json
{
  "idempotency_key": "kiln-tips-v1",
  "items": [
    {
      "mode": "exact",
      "format": "rich",
      "external_id": "kiln-tips",
      "text": "# Three kiln mistakes beginners make\n\n%%MEDIA:0%%\n\n- opening the kiln too early\n- glaze on the foot of the cup\n- packing pieces so they touch\n\nAll three are easy to avoid once you know them.",
      "media": [{ "type": "photo", "url": "https://example.com/kiln.jpg" }],
      "threads": [
        { "text": "THREE KILN MISTAKES THAT RUIN YOUR FIRST CUPS", "media": [{ "type": "photo", "url": "https://example.com/kiln.jpg" }] },
        { "text": "One: opening the kiln too early. The sudden cold cracks the glaze. Wait until it is warm, not hot." },
        { "text": "Two: glaze on the foot. It melts onto the shelf. Wipe the bottom clean before firing." },
        { "text": "Three: pieces that touch fuse together. Leave a finger of space between them." }
      ]
    }
  ]
}
```

The draft arrives in the author's chat with a Threads tab; the author publishes it from there. PostFrog's AI does not touch `threads`. `get_batch` returns it in `threads`, and `threads_publication` once it is published.