Skip to main content
The Hagaki Renderer exposes a simple HTTP API: encode a JSON payload as a base64 hash, pass it as a URL path segment, and receive a PNG image in the response. This guide walks you through your first card render and a fan spread example.

Prerequisites

Before making requests, ensure the following are in place:
  • The Hagaki Renderer service is running and reachable on port 8899.
  • Asset directories are present at the expected paths relative to the service binary:
    • ../asset/private/frame — frame images
    • ../asset/private/idol — character images
    • ../asset/public/render — output directory for cached renders
The service binds to 0.0.0.0:8899 by default. If you are running it behind a reverse proxy or in a container, substitute your actual host and port in the examples below.

Render a single card

1

Construct the card payload

Build a JSON object describing the card you want to render. All fields except offset_x, offset_y, and save_name are required.
card-payload.json
{
  "id": 42,
  "variant": 0,
  "dye": 0,
  "kindled": false,
  "frame_type": 0
}
FieldTypeDescription
idu32Character ID.
variantu8Card variant index.
dyeu32Dye color value applied to supported frames.
kindledbooleanWhether the kindled effect is active.
frame_typeu8Frame to use: 0 = Moonweaver, 1 = Essentia, 2 = Snowglow.
offset_xi32?Optional horizontal offset for the character image in pixels.
offset_yi32?Optional vertical offset for the character image in pixels.
save_namestring?Optional filename (without extension) for the cached render on disk.
Output cards are always rendered at 550×800 px. The offset_x and offset_y fields are accepted and parsed but are not currently applied in the render pipeline — they are reserved for future use.
2

Encode the payload as a base64 hash

Serialize the JSON to a string and encode it as standard base64, no padding. This encoded string becomes the {hash} path segment.
const payload = {
  id: 42,
  variant: 0,
  dye: 0,
  kindled: false,
  frame_type: 0,
};

const hash = Buffer.from(JSON.stringify(payload)).toString('base64').replace(/=+$/, '');
console.log(hash);
// eyJpZCI6NDIsInZhcmlhbnQiOjAsImR5ZSI6MCwia2luZGxlZCI6ZmFsc2UsImZyYW1lX3R5cGUiOjB9
The service decodes using the standard base64 alphabet (+ and /), with no padding. Strip the trailing = padding but do not replace + or /. Most shell base64 utilities produce the standard alphabet by default, so only the padding removal (tr -d '=') is needed.
3

Make the request

Pass the hash as the final path segment of the /render/card/ endpoint.
curl -o card.png \
  "http://localhost:8899/render/card/eyJpZCI6NDIsInZhcmlhbnQiOjAsImR5ZSI6MCwia2luZGxlZCI6ZmFsc2UsImZyYW1lX3R5cGUiOjB9"
4

Interpret the response headers

A successful response returns 200 OK with a PNG body and the following headers:
HeaderExample valueDescription
Content-Typeimage/pngAlways PNG.
X-Sourcerendered on requestrendered on request for a fresh render; loaded from disk cache when the PNG was already saved.
X-Processing-Time12.34msWall-clock time the renderer took to produce the image.
Inspect headers with curl
curl -sI \
  "http://localhost:8899/render/card/eyJpZCI6NDIsInZhcmlhbnQiOjAsImR5ZSI6MCwia2luZGxlZCI6ZmFsc2UsImZyYW1lX3R5cGUiOjB9"
If you include a save_name field in the payload, Hagaki saves the rendered PNG to ../asset/public/render/{save_name}. Subsequent requests with the same save_name return X-Source: loaded from disk cache with a near-zero X-Processing-Time. Without save_name, nothing is saved and the image is re-rendered on every request.

Render a fan spread

A fan render composes multiple cards into a single fanned-out image. The payload wraps a cards array using the same per-card structure as above, plus an optional top-level save_name.
1

Build the fan payload

fan-payload.json
{
  "cards": [
    { "id": 1, "variant": 0, "dye": 0, "kindled": false, "frame_type": 0 },
    { "id": 2, "variant": 1, "dye": 0, "kindled": true,  "frame_type": 1 },
    { "id": 3, "variant": 0, "dye": 0, "kindled": false, "frame_type": 2 }
  ]
}
2

Encode and request

const fanPayload = {
  cards: [
    { id: 1, variant: 0, dye: 0, kindled: false, frame_type: 0 },
    { id: 2, variant: 1, dye: 0, kindled: true,  frame_type: 1 },
    { id: 3, variant: 0, dye: 0, kindled: false, frame_type: 2 },
  ],
};

const hash = Buffer.from(JSON.stringify(fanPayload)).toString('base64').replace(/=+$/, '');
const response = await fetch(`http://localhost:8899/render/fan/${hash}`);
The fan layout applies a 5° tilt between adjacent cards arranged along a circular arc with a center distance of 3000 px. The more cards you include, the wider the spread.

Error responses

StatusMeaning
400The hash could not be decoded, or the decoded JSON does not match the expected shape.
500The render timed out (limit: 5 seconds), a required asset file is missing, or the PNG buffer could not be written.
418The request path did not match any known endpoint.

Next steps

API reference

Full parameter reference for all three endpoints: card, fan, and album.

Frame types

Detailed breakdown of Moonweaver, Essentia, and Snowglow frame capabilities.

Build docs developers (and LLMs) love