api docs

back to converter

overview

send a Discohook JSON export and get bleed.bot scripting syntax back in a single HTTP request

base url

https://d2bleed.pages.dev

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, or components
  • an export containing a message at data
  • an export containing messages at messages. conversion reads messages[0].data only

include attachments from the original export when components reference uploaded images

response

{
  "script": "{text: hello world}",
  "warnings": []
}
  • script is the generated bleed.bot script as a string. it can be empty if there was no convertible content
  • warnings is an array of strings. read it even when the response status is 200

error responses

conversion errors use a JSON object with an error string

{ "error": "request body must contain valid json" }
400 invalid JSON
request body must contain valid json — send a valid UTF-8 JSON body
400 invalid request structure
the data property must contain a discohook json object — pass an object in data
400 conversion failed for the input
the data does not match a discohook message — check the shape of the message and its components
405 unsupported method
use post to convert a discohook export — use POST as indicated by the Allow header
413 request too large
request too large — reduce the JSON body size
422 generated script too large
the generated script is too large — split the input into smaller conversions
429 too many requests
rate limit exceeded — wait for the time suggested by Retry-After
502 conversion service unavailable
could not reach the conversion service
503 conversion service unavailable
conversion 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 content for the text. becomes {text: …} at the message level or {content: …} inside a container. empty text is skipped
container 17
put children in components. optional accent_color sets the color and spoiler marks it as a spoiler. a container with no text block gets an empty one and a warning
section 9
put text displays in components and a thumbnail or button in the optional accessory. 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}. set spacing to 2 for a large gap and divider to false for 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.url in items. the gallery keeps the first 10 usable URLs and drops item descriptions and spoiler settings
button 2
use a style and a label or emoji. link buttons use style 5 with a url. styles 1–4, emoji, and disabled are preserved in scripting syntax. custom_id and 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 options array. placeholder, min_values, and max_values are optional. each option can use label, value, description, and emoji. only the first 25 options are kept, with a warning when truncated
legacy embeds
use the message’s embeds array. 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

JavaScript fetch
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

Node.js
// 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);
Python
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
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://filename reference 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