Skip to main content
Every Hagaki endpoint uses a single URL path segment — the hash — to fully describe the render. There is no request body and no query string. The hash is a base64-encoded JSON object containing all render parameters.
GET /render/card/{hash}

Why this design

Encoding the full payload in the URL has three practical benefits:
  • Reproducibility — the same hash always produces the same image. You can store a hash and replay the exact same render later.
  • Stateless server — Hagaki holds no session state. Any instance can serve any request.
  • HTTP caching — because the hash is in the URL path, standard HTTP caches (Nginx proxy_cache, CDNs, browser caches) treat each unique render as a distinct cacheable resource.

Encoding process

1

Build the JSON payload

Construct a JSON object with the render parameters you need. All whitespace is irrelevant — the encoded form is what matters.
payload.json
{
  "id": 42,
  "variant": 0,
  "dye": 16711680,
  "kindled": true,
  "frame_type": 0
}
2

Serialize to a UTF-8 string

Convert the object to a compact JSON string. Key order does not matter to the renderer, but it affects the resulting hash — use consistent serialization if you want stable hashes for caching.
{"id":42,"variant":0,"dye":16711680,"kindled":true,"frame_type":0}
3

Encode as base64 (no padding)

Encode the UTF-8 bytes of the JSON string using base64 standard alphabet, no padding.
const payload = {
  id: 42,
  variant: 0,
  dye: 16711680,
  kindled: true,
  frame_type: 0,
};

// Use 'base64' (standard alphabet: +/), then strip the trailing '=' padding.
// Do NOT use 'base64url' — it substitutes '-' and '_' which the Rust decoder won't accept.
const hash = Buffer.from(JSON.stringify(payload)).toString('base64').replace(/=+$/, '');
console.log(hash);
// eyJpZCI6NDIsInZhcmlhbnQiOjAsImR5ZSI6MTY3MTE2ODAsImtpbmRsZWQiOnRydWUsImZyYW1lX3R5cGUiOjB9
4

Use the hash in the URL

Append the hash directly to the endpoint path. No additional URL encoding is required.
curl
curl -o card.png \
  "http://localhost:8899/render/card/eyJpZCI6NDIsInZhcmlhbnQiOjAsImR5ZSI6MTY3MTE2ODAsImtpbmRsZWQiOnRydWUsImZyYW1lX3R5cGUiOjB9"

base64url vs standard no-pad

The Rust decoder uses base64 STANDARD_NO_PAD, which is the standard alphabet (A–Z, a–z, 0–9, +, /) with no padding. Node.js Buffer.toString('base64url') uses the URL-safe alphabet (- instead of +, _ instead of /) and also strips padding. These two alphabets differ at positions 62 and 63:
PositionStandard (STANDARD_NO_PAD)URL-safe (base64url)
62+-
63/_
The bench.js bundled with the project uses Node.js base64url encoding, which produces - and _ instead of + and /. The Rust decoder uses STANDARD_NO_PAD, which expects + and /. These are incompatible for any payload that maps to byte values at positions 62 or 63.In practice, most short JSON payloads do not produce these characters in their base64 output, so base64url hashes from Node.js typically decode successfully. However, this is not guaranteed — you should use the standard no-pad alphabet (+//, no =) to be safe with all payloads.The shell encoding examples (using tr '+/' '-_') produce URL-safe output, which may similarly fail for edge-case payloads. Replace tr '+/' '-_' | tr -d '=' with tr -d '=' to use the standard alphabet in shell scripts.

Character safety in URLs

Base64 output (both standard and URL-safe variants, without padding) contains only alphanumeric characters plus at most +, /, -, or _. None of these characters require percent-encoding in a URL path segment. You can use the hash directly in the URL without any additional escaping.

Decoding a hash for debugging

To inspect the payload inside an existing hash, reverse the process:
const hash = 'eyJpZCI6NDIsInZhcmlhbnQiOjAsImR5ZSI6MTY3MTE2ODAsImtpbmRsZWQiOnRydWUsImZyYW1lX3R5cGUiOjB9';
// Use 'base64' to decode the standard alphabet
const payload = JSON.parse(Buffer.from(hash, 'base64').toString('utf-8'));
console.log(payload);
// { id: 42, variant: 0, dye: 16711680, kindled: true, frame_type: 0 }
Decoding a hash in your browser console or a REPL is the fastest way to verify that your client is encoding payloads correctly before making a live request.

Build docs developers (and LLMs) love