turboyap

Discord multi-image message API

Post 2 to 10 images in one Discord message through an API or MCP server: attachment limits, alt text, 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 runs the same requests against a simulated Discord account.

What a Discord multi-image message is

A Discord multi-image message carries 2 to 10 image attachments in one message, sent to a channel through its webhook. In Turboyap it's type: "slideshow" sent to a Discord account.

Turboyap keeps each image under 1 MB so the whole message fits Discord's 10 MiB default upload limit, and sends each image's alt text as its attachment description.

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), and each value links to the Discord documentation it comes from.

LimitValueSource
Images per post2–10Discord: uploading files
FormatsJPEG, PNG, WebP, GIF (Turboyap converts anything else to JPEG)Discord: uploading files
File size1 MB (1,000,000 bytes) per imageDiscord: uploading files
DimensionsFits 2560×2560 (Turboyap resizes to fit)Discord: uploading files
Aspect ratioAnyDiscord: uploading files
Caption2,000 characters (sent as content)Discord: execute webhook
Link cardYes: pass link, sent as embed (title, url, description)Discord: embed limits
Rate limitWebhook limits are set per webhook and change over time; on a 429 Turboyap waits the retry_after Discord sendsDiscord: rate limits

Compare with other platforms in the social media API limits tracker.

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

# 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": "slideshow",
    "media_ids": ["med_…", "med_…", "med_…"],
    "caption": "Five label tricks brands hope you miss.",
    "targets": [{ "account_id": "acct_discord…" }],
    "schedule_at": "2026-10-20T18:00:00-04:00",
    "fit": { "overflow": "truncate" },
    "idempotency_key": "discord-multi-image-message-2026-10-20",
    "dry_run": true
  }'

TypeScript with @turboyap/sdk

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 ["slide-1.png", "slide-2.png", "slide-3.png"]) {
  const { media } = await ty.uploadMedia(await readFile(file), { filename: file, contentType: "image/png" });
  media_ids.push(media.id);
}

const post = {
  type: "slideshow" as const,
  media_ids,
  caption: "Five label tricks brands hope you miss.",
  targets: [{ account_id: account.id }],
  schedule_at: "2026-10-20T18:00:00-04:00",
  fit: { overflow: "truncate" as const },
  idempotency_key: "discord-multi-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:

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

Then ask:

Upload the slides in ~/content/labels and post them in one message to our Discord channel on Thursday at noon ET. Dry-run first and fix anything Discord rejects.

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.

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.

CodeWhenFix
too_many_itemsMore than 10 images for Discord.Set fit.overflow to "truncate" to post the first 10, or pass targets[i].media_ids with your best 10 slides (keep your call-to-action slide).
too_few_itemsFewer than 2 images. One slide posts as a single image instead, with a warning.Add media so there are at least 2, or send it as an image post.
caption_too_longCaption over 2,000 characters.Shorten it, or pass targets[i].caption with a version under 2,000 characters.
media_format_unsupportedA 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_pastschedule_at is more than a minute in the past.Use "now" or a future time with a timezone offset.
account_unavailableThe Discord account needs to be reconnected.Reconnect the account, then retry.
unsupported_platformA target is on a platform Turboyap doesn't support yet.Remove that target. Supported platforms: tiktok, instagram, bluesky (beta), telegram (beta), discord (beta).
insufficient_scopeThe 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_allowedThe 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_scopeAn 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_outcomeTurboyap 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

Read more in the autonomy dial.

FAQ

How many images can one Discord message have?

Up to 10 attachments. Send more and Turboyap returns too_many_items; set fit.overflow to "truncate" to post the first 10.

Why does Turboyap shrink images for Discord?

A webhook message has to fit Discord's 10 MiB default upload limit with all its attachments. Keeping each of up to 10 images under 1 MB means the message always fits.

Can I post the same images to Discord, Telegram and Bluesky at once?

Yes, in one request with one target per account. With fit.overflow: "truncate", Discord and Telegram each get the first 10 and Bluesky the first 4.

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.