overview
send a Discohook JSON export and get bleed.bot scripting syntax back in a single HTTP request
base url
use a relative path when calling from the converter’s origin, or the full URL from a server or command line
authentication
the conversion endpoint is public and requires no credentials
endpoint
POST /api/convert
Content-Type: application/json
send a JSON object with a data property and read a JSON response with script and warnings
successful requests return 200. the entire UTF-8 request body, including the data wrapper, can be up to 262,144 bytes (256 KiB)
request body
{
"data": {
"components": [
{ "type": 10, "content": "hello world" }
]
}
}
data is the parsed export object. pass the object as-is, including its original wrapper if it has one
- a message object with
content,embeds, orcomponents - an export containing a message at
data - an export containing messages at
messages. conversion readsmessages[0].dataonly
include attachments from the original export when components reference uploaded images
response
{
"script": "{text: hello world}",
"warnings": []
}
scriptis the generated bleed.bot script as a string. it can be empty if there was no convertible contentwarningsis an array of strings. read it even when the response status is200
error responses
conversion errors use a JSON object with an error string
{ "error": "request body must contain valid json" }
400invalid JSONrequest body must contain valid json— send a valid UTF-8 JSON body400invalid request structurethe data property must contain a discohook json object— pass an object indata400conversion failed for the inputthe data does not match a discohook message— check the shape of the message and its components405unsupported methoduse post to convert a discohook export— usePOSTas indicated by theAllowheader413request too largerequest too large— reduce the JSON body size422generated script too largethe generated script is too large— split the input into smaller conversions429too many requestsrate limit exceeded— wait for the time suggested byRetry-After502conversion service unavailablecould not reach the conversion service503conversion service unavailableconversion service unavailable
check the HTTP status and response type before using the body. infrastructure errors can return a non-JSON response
supported components
use numeric Discord component type values inside components
- text display
10 - use
contentfor the text. becomes{text: …}at the message level or{content: …}inside a container. empty text is skipped - container
17 - put children in
components. optionalaccent_colorsets the color andspoilermarks it as a spoiler. a container with no text block gets an empty one and a warning - section
9 - put text displays in
componentsand a thumbnail or button in the optionalaccessory. text displays are joined with line breaks when they share a converted accessory. a standalone section with an accessory is wrapped in a container - separator
14 - becomes
{separator}. setspacingto2for a large gap anddividertofalsefor an empty gap - thumbnail
11 - provide
media.url. the converter emits{thumbnail: …}and adds the text block number for section thumbnails after the first block - media gallery
12 - put entries with
media.urlinitems. the gallery keeps the first 10 usable URLs and drops item descriptions and spoiler settings - button
2 - use a
styleand alabeloremoji. link buttons use style5with aurl. styles1–4, emoji, anddisabledare preserved in scripting syntax.custom_idand Discord interactions are not carried over - action row
1 - put buttons or string select menus in
components. the row assigns their scripting position. other child types are skipped - string select
3 - use inside an action row with an
optionsarray.placeholder,min_values, andmax_valuesare optional. each option can uselabel,value,description, andemoji. only the first 25 options are kept, with a warning when truncated - legacy embeds
- use the message’s
embedsarray. each embed becomes a container. title, description, author name, fields, footer text, and a valid timestamp become text. color, thumbnail, and image are converted. inline field layout and author or footer icons are not carried over
code examples
const response = await fetch('/api/convert', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ data: { content: 'hello world' } })
});
const result = await response.json().catch(() => null);
if (!response.ok) {
throw new Error(result?.error ?? ('HTTP ' + response.status));
}
if (typeof result?.script !== 'string') {
throw new Error('invalid conversion response');
}
console.log(result.script);
run the browser example on the converter’s origin. for a site on another origin, call the API from your server
// save as convert.mjs and run with node convert.mjs
const response = await fetch('https://d2bleed.pages.dev/api/convert', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ data: { content: 'hello world' } })
});
const result = await response.json().catch(() => null);
if (!response.ok) {
throw new Error(result?.error ?? ('HTTP ' + response.status));
}
if (typeof result?.script !== 'string') {
throw new Error('invalid conversion response');
}
console.log(result.script);
import json
from urllib.error import HTTPError
from urllib.request import Request, urlopen
request = Request(
"https://d2bleed.pages.dev/api/convert",
data=json.dumps({"data": {"content": "hello world"}}).encode(),
headers={"Content-Type": "application/json"},
method="POST",
)
try:
with urlopen(request, timeout=15) as response:
result = json.load(response)
except HTTPError as error:
detail = error.read().decode(errors="replace")
raise SystemExit(f"HTTP {error.code}: {detail}") from error
print(result["script"])
curl --silent --show-error --fail-with-body \
'https://d2bleed.pages.dev/api/convert' \
--header 'Content-Type: application/json' \
--data '{"data":{"content":"hello world"}}'
cURL prints the JSON response, including script, and exits with a failure status for HTTP errors
warnings and limitations
- unsupported component types are skipped and may produce no individual warning. review the generated script before using it
- for images, use a public HTTP or HTTPS URL. an
attachment://filenamereference can be resolved from matching attachment metadata or a usable proxy URL - browser-only
blob:URLs and localhost URLs cannot serve as public images. if there is no usable alternative, the image is skipped and a warning is returned - the response contains scripting text. it does not upload attachments, post messages, or confirm that image URLs still work
rate limits and usage
the maximum json request body is 256 kib (262,144 bytes), including the data wrapper. larger requests return 413
both limits apply per client: 10 requests per 10 seconds and 30 requests per minute. callers behind a shared ip share a quota, and requests without a client ip share a fallback quota
cloudflare counters are approximate and enforced per cloudflare location. there is no daily application-level per-client quota
on 429, Retry-After suggests waiting 10 or 60 seconds based on the window that rejected the request
scripts above 2,097,152 utf-16 code units are rejected