# Discord image message API

Post an image to a Discord channel through an API or MCP server: attachment size and formats, alt text, link embeds, example requests, and errors with fixes.

**Beta:** you can connect a Discord account and post to it through the API in early access today. It hasn't been proven with a real production post yet. The `@turboyap/sdk` and `@turboyap/mcp` packages call the same API. [Test mode](https://turboyap.com/docs/test-mode) runs the same requests against a simulated Discord account.

## What a Discord image message is

A Discord image message is a message with one image attached, sent to a channel through its webhook. In Turboyap it's `type: "image"` with one media id, and the caption becomes the message text.

Turboyap uploads the image to Discord as an attachment, with its alt text from `alt_text` as the attachment's description, so the message keeps its image after Turboyap deletes its own copy.

## Limits

What Discord accepts for this post type. Turboyap's planner checks every one of these before a post is scheduled (the same data as [platform rules](https://turboyap.com/docs/platform-rules)), and each value links to the Discord documentation it comes from.

| Limit | Value | Source |
|---|---|---|
| Images per post | 1 | [Discord: uploading files](https://docs.discord.com/developers/reference#uploading-files) |
| Formats | JPEG, PNG, WebP, GIF (Turboyap converts anything else to JPEG) | [Discord: uploading files](https://docs.discord.com/developers/reference#uploading-files) |
| File size | 1 MB (1,000,000 bytes) per image | [Discord: uploading files](https://docs.discord.com/developers/reference#uploading-files) |
| Dimensions | Fits 2560×2560 (Turboyap resizes to fit) | [Discord: uploading files](https://docs.discord.com/developers/reference#uploading-files) |
| Aspect ratio | Any | [Discord: uploading files](https://docs.discord.com/developers/reference#uploading-files) |
| Caption | 2,000 characters (sent as `content`) | [Discord: execute webhook](https://docs.discord.com/developers/resources/webhook#execute-webhook) |
| Link card | Yes: pass `link`, sent as `embed` (title, url, description) | [Discord: embed limits](https://docs.discord.com/developers/resources/message#embed-object-embed-limits) |
| Rate limit | Webhook limits are set per webhook and change over time; on a 429 Turboyap waits the retry_after Discord sends | [Discord: rate limits](https://docs.discord.com/developers/topics/rate-limits) |

Compare with other platforms in the [social media API limits tracker](https://turboyap.com/tools/social-api-limits#discord).

## Connect a Discord account

1. Open a connect link (from `create_connect_link`, or `POST /v1/connect_links` with `{ "platform": "discord" }`) and continue to Discord.
2. On Discord's own authorization screen, pick the server and the text or announcement channel Turboyap should post to, then authorize. Forum channels aren't supported yet.
3. Discord creates a webhook for that channel and hands it to Turboyap. The account is connected.

Turboyap asks Discord only for the `webhook.incoming` scope. No bot joins your server, the webhook can post only to the channel you picked, and Turboyap doesn't keep the OAuth access token. Disconnecting the account in Turboyap (`DELETE /v1/accounts/:id`) deletes the webhook.

An agent can make the connect link and send it to the person who manages the server. New accounts start on autonomy level 1, so agent posts wait for approval.

## Example requests

All three do the same thing: upload the media, preview the post with a dry run, then schedule it. The idempotency key makes retries safe: send the same request again with the same key and you get the original post back, not a second one.

**Early access:** `api.turboyap.com` is open to early-access users only, and `@turboyap/sdk` and `@turboyap/mcp` aren't on npm yet. These examples show the API as built.

### curl

```bash
# 1. Upload each image. The body is the raw file; Content-Type is its type.
curl https://api.turboyap.com/v1/media \
  -H "Authorization: Bearer $TURBOYAP_API_KEY" \
  -H "Content-Type: image/png" \
  -H "X-Filename: slide-1.png" \
  --data-binary @slide-1.png
# → { "media": { "id": "med_…", "width": 1080, "height": 1920, … }, "deduplicated": false }

# 2. Preview. Remove "dry_run" to create and schedule the post.
curl https://api.turboyap.com/v1/posts \
  -H "Authorization: Bearer $TURBOYAP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "image",
    "media_ids": ["med_…"],
    "caption": "Read the label before you buy.",
    "link": { "url": "https://example.com/labels", "title": "How to read an ice cream label" },
    "targets": [{ "account_id": "acct_discord…" }],
    "schedule_at": "2026-10-20T18:00:00-04:00",
    "idempotency_key": "discord-image-message-2026-10-20",
    "dry_run": true
  }'
```

### TypeScript with @turboyap/sdk

```ts
import { readFile } from "node:fs/promises";
import { TurboyapClient } from "@turboyap/sdk";

const ty = new TurboyapClient({ apiKey: process.env.TURBOYAP_API_KEY! });
const account = (await ty.listAccounts()).find((a) => a.platform === "discord")!;

const media_ids: string[] = [];
for (const file of ["image.png"]) {
  const { media } = await ty.uploadMedia(await readFile(file), { filename: file, contentType: "image/png" });
  media_ids.push(media.id);
}

const post = {
  type: "image" as const,
  media_ids,
  caption: "Read the label before you buy.",
  link: { "url": "https://example.com/labels", "title": "How to read an ice cream label" },
  targets: [{ account_id: account.id }],
  schedule_at: "2026-10-20T18:00:00-04:00",
  idempotency_key: "discord-image-message-2026-10-20",
};

// Preview first: every problem comes back with a fix.
const preview = await ty.createPost({ ...post, dry_run: true });
if (!preview.ok) throw new Error(preview.errors.map((e) => `${e.code}: ${e.fix}`).join("\n"));

const { post: created } = await ty.createPost(post);
console.log(created.targets[0]?.status, created.targets[0]?.next_step);
```

### Claude Code (MCP)

Add the Turboyap MCP server once, with an agent key from your workspace:

```bash
claude mcp add turboyap -e TURBOYAP_API_KEY=ty_agent_… -- npx -y @turboyap/mcp
```

Then ask:

> Post ~/content/label-check.png to our Discord channel tomorrow at 8am ET with the caption "Read the label before you buy." and alt text that describes it. Dry-run first.

The agent calls `list_accounts`, `get_platform_rules`, `upload_media`, `create_post` with `dry_run: true`, applies any fixes, calls `create_post` again to schedule, and reports each account's `next_step` from `get_post`. The same server works in Cursor; see the [MCP quickstart](https://turboyap.com/docs/mcp-quickstart).

## How publishing works

When the scheduled time arrives, Turboyap executes the channel's webhook with `?wait=true`, with the images as attachments, so Discord confirms the message and returns its id. Publishing is immediate: the target moves `scheduled` → `publishing` → `published`, and `platform_url` links to the message. Discord's webhooks have no idempotency key, so if a response is lost Turboyap marks the target `unknown_outcome` instead of posting again.

Mentions in the text don't notify anyone: Turboyap sends an empty allowed_mentions, so an agent can't ping @everyone.

Images go as attachments, up to 10 per message, each kept under 1 MB so the message fits Discord's 10 MiB default upload limit.

Pass `link` to add an embed with its title and description. Discord also previews links in the text on its own.

## Common errors and fixes

Every error has a `code`, the `field` that caused it, and a `fix` an agent can apply without asking you. A request with several problems returns all of them in `issues`; a dry run returns them without creating anything.

| Code | When | Fix |
|---|---|---|
| `too_many_items` | More than one media id on an image post. | Send one media id, or set `fit.overflow` to `"truncate"` to post only the first. For several images, use `type: "slideshow"`. |
| `caption_too_long` | Caption over 2,000 characters. | Shorten it, or pass `targets[i].caption` with a version under 2,000 characters. |
| `media_format_unsupported` | A media id points to a video, or the upload isn't an image Turboyap can read. | Upload PNG, JPEG or WebP images. Video posts are coming soon. |
| `schedule_in_past` | `schedule_at` is more than a minute in the past. | Use `"now"` or a future time with a timezone offset. |
| `account_unavailable` | The Discord account needs to be reconnected. | Reconnect the account, then retry. |
| `unsupported_platform` | A target is on a platform Turboyap doesn't support yet. | Remove that target. Supported platforms: tiktok, instagram, bluesky (beta), telegram (beta), discord (beta). |
| `insufficient_scope` | The key lacks a scope the request needs, such as `publish` for `schedule_at: "now"` (agent keys don't have it by default). | Schedule a future time instead of `"now"`, omit `schedule_at` to save a draft, or use a key with the missing scope. `GET /v1/keys/me` lists the key's scopes. |
| `account_not_allowed` | The key is limited to some accounts, and a target isn't one of them. | Remove that target, or use a key whose `account_ids` include the account. |
| `forbidden_scope` | An agent key tries to approve or reject a post. | A human approves agent posts. To skip review for an account, a human sets its autonomy level to 3. |
| `unknown_outcome` | Turboyap stopped partway through publishing (for example, a restart) and can't confirm whether the post went out. It doesn't publish it again. | Check the account on the platform. If the post isn't there, create it again. |

Errors from Discord itself appear on the target's `error` with Discord's own code. Turboyap retries rate limits and temporary failures with backoff, and marks the account for reconnection when the webhook is deleted.

## Approval and audit

- **Level 1, review everything (default):** a post an agent schedules starts as `pending_approval` and waits until a human approves it with a human key (`POST /v1/posts/:id/approve`). Agent keys can't approve.
- **Level 3, full autopilot:** agent posts schedule directly. Every action is still logged. Level 2, guardrails, is coming soon.
- **Posts created by humans** schedule directly at any level.
- **Edits re-open review:** if an agent changes the caption or time of an approved post on a level-1 account, it goes back to `pending_approval`.
- **Dry runs show the outcome:** each target's `status_on_create` tells the agent whether the post will wait for approval.
- **Audit log:** every create, approval, edit, publish and failure is recorded with whether a human or an agent did it, the key id, and the MCP client name. The kill switch (`POST /v1/workspace/pause`) stops every publish to Discord and every other platform at once.

Read more in [the autonomy dial](https://turboyap.com/docs/autonomy-dial).

## FAQ

### What image size can I send through a Discord webhook?

Turboyap keeps each image under 1 MB (1,000,000 bytes) and within 2560×2560 pixels, so a message with up to 10 images fits Discord's 10 MiB default upload limit. JPEG, PNG, WebP and GIF are accepted as-is, and anything else is converted to JPEG.

### Does the image stay in Discord after Turboyap deletes its media?

Yes. Turboyap uploads images to Discord as attachments instead of linking to them, so the message keeps its image.

### Can I add a link embed to a Discord image message?

Yes. Pass `link` with a `url` and an optional `title` and `description`, and Turboyap adds it to the message as an embed.

## Related

- [Discord message API](https://turboyap.com/api/discord/message) (beta)
- [Discord multi-image message API](https://turboyap.com/api/discord/multi-image-message) (beta)
- [TikTok photo post API](https://turboyap.com/api/tiktok/photo-post)
- [Instagram image post API](https://turboyap.com/api/instagram/image-post)
- [Bluesky image post API](https://turboyap.com/api/bluesky/image-post) (beta)
- [Telegram photo post API](https://turboyap.com/api/telegram/photo) (beta)
- [Post one slideshow to TikTok and Instagram](https://turboyap.com/docs/slideshows)
- [REST API reference](https://turboyap.com/docs/api)
- [Platform rules](https://turboyap.com/docs/platform-rules)
- [Social media API limits tracker](https://turboyap.com/tools/social-api-limits)
