Discord message API
Post messages to a Discord channel through an API or MCP server: the 2,000-character limit, link embeds, connecting with a webhook and no bot, 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 message is
A Discord message here is a post of up to 2,000 characters in a server's text or announcement channel, sent through that channel's webhook. In Turboyap it's type: "text", and the caption is the message.
You connect by authorizing Turboyap on Discord's own screen and picking the server and channel; Discord creates the webhook, and no bot joins your server. Pass link to add an embed with a title and 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.
| Limit | Value | Source |
|---|---|---|
| Media | None (text only) | Discord: execute webhook |
| Text | 2,000 characters (sent as content) | Discord: execute webhook |
| Link card | Yes: pass link, sent as embed (title, url, description) | Discord: 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 |
Compare with other platforms in the social media API limits tracker.
Connect a Discord account
- Open a connect link (from
create_connect_link, orPOST /v1/connect_linkswith{ "platform": "discord" }) and continue to Discord. - 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.
- 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: 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
# 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": "text",
"caption": "New guide: how to tell ice cream from frozen dairy dessert.",
"link": { "url": "https://example.com/labels", "title": "How to read an ice cream label", "description": "Three things to check before you buy." },
"targets": [{ "account_id": "acct_discord…" }],
"schedule_at": "2026-10-20T18:00:00-04:00",
"idempotency_key": "discord-message-2026-10-20",
"dry_run": true
}' TypeScript with @turboyap/sdk
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 post = {
type: "text" as const,
caption: "New guide: how to tell ice cream from frozen dairy dessert.",
link: { "url": "https://example.com/labels", "title": "How to read an ice cream label", "description": "Three things to check before you buy." },
targets: [{ account_id: account.id }],
schedule_at: "2026-10-20T18:00:00-04:00",
idempotency_key: "discord-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:
Post "New guide: how to tell ice cream from frozen dairy dessert." to our Discord announcements channel tomorrow at 9am ET with an embed for https://example.com/labels. Dry-run first.
The agent calls list_accounts, get_platform_rules, 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, 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.
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 | Media ids on a text post. Text posts take no media. | Use type: "image" (one image) or "slideshow" (several) to post media, or remove media_ids. |
invalid_request | A text post with no text and no link. | Set caption to the text of the post. |
caption_too_long | Text over 2,000 characters. | Shorten it, or pass targets[i].caption with a version under 2,000 characters. |
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_approvaland 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_createtells 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.
FAQ
How long can a Discord message be through a webhook?
2,000 characters. Turboyap checks the length before scheduling and returns caption_too_long with a fix. A link embed's title is trimmed to 256 characters, Discord's limit.
Does Turboyap need a bot in my Discord server?
No. Turboyap asks Discord only for the webhook.incoming scope: you pick a server and channel on Discord's screen, and Discord creates a webhook that posts to that one channel. Disconnecting the account in Turboyap deletes the webhook.
Can an agent ping @everyone through Turboyap?
No. Turboyap sends every Discord message with an empty allowed_mentions, so mentions in the text, @everyone included, don't notify anyone.
Can I post to a Discord forum channel?
Not yet. Connect a text or announcement channel instead.
Related
- Discord image message API (beta)
- Discord multi-image message API (beta)
- Bluesky text post API (beta)
- Telegram text message API (beta)
- REST API reference
- Platform rules
- Social media API limits tracker
This page as markdown: /api/discord/message.md. Everything for LLMs: /llms.txt.