back

API Reference

// every endpoint of the server, v0

~/serbekun/api $ man api --version 0
base url

no endpoints match the filter.

01 — general

All API responses are JSON unless stated otherwise. Errors use the shape {"error": "...", "message": "..."}, where message is omitted when absent. Auth is registered per endpoint but currently disabled for all of them — no Authorization header is required. Resource tokens (repository, short url, file) are separate from that and always required where listed.

get /api/v0/version

Returns the build version of the running server.

response 200
{
  "version": "1.4.0"
}
example
curl -s $BASE/api/v0/version
02 — cipher / aes

AES-GCM helpers — symmetric, so both sides share one key. Keys and payloads travel as Base64 strings and the server never stores them. See cipher / rsa for public-key encryption and signatures.

get /api/v0/cipher/aes

Generates a fresh AES key. The key is returned once and never persisted.

response 200
{
  "key": "b64-encoded-aes-key"
}
statuses
200key generated 500key generation failed
example
curl -s $BASE/api/v0/cipher/aes
post /api/v0/cipher/aes/encrypt

Encrypts data with the given AES key.

body — application/json
fieldtypedescription
datastringrequiredplain text to encrypt
keystringrequiredBase64 AES key
response 200
{
  "data": "b64-cipher-text"
}
statuses
200encrypted 400data or key blank 500encryption failed
example
curl -s -X POST $BASE/api/v0/cipher/aes/encrypt \
  -H 'Content-Type: application/json' \
  -d '{"data":"hello","key":"<key>"}'
post /api/v0/cipher/aes/decrypt

Decrypts data produced by the encrypt endpoint using the same key.

body — application/json
fieldtypedescription
datastringrequiredBase64 cipher text
keystringrequiredthe same Base64 AES key
response 200
{
  "data": "hello"
}
statuses
200decrypted 400data or key blank 500wrong key or corrupted payload
example
curl -s -X POST $BASE/api/v0/cipher/aes/decrypt \
  -H 'Content-Type: application/json' \
  -d '{"data":"<cipher>","key":"<key>"}'
03 — cipher / rsa

RSA-2048 with OAEP-SHA256 padding for encryption and SHA256withRSA for signatures. Keys are Base64 DER — X.509 for the public key, PKCS#8 for the private one — and the server keeps neither: whatever a call needs must be sent with it. Direct RSA encryption fits at most 190 bytes; anything larger belongs in cipher / hybrid.

get /api/v0/cipher/rsa/keypair

Generates a fresh 2048-bit RSA key pair. Both keys are returned once and never persisted.

response 200
{
  "publicKey": "b64-x509-public-key",
  "privateKey": "b64-pkcs8-private-key"
}
statuses
200key pair generated 500key generation failed
example
curl -s $BASE/api/v0/cipher/rsa/keypair
post /api/v0/cipher/rsa/encrypt

Encrypts data with an RSA publicKey. Only the holder of the matching private key can read the result.

body — application/json
fieldtypedescription
datastringrequiredBase64 payload, at most 190 bytes decoded
publicKeystringrequiredBase64 X.509 RSA public key
response 200
{
  "data": "b64-cipher-text"
}
statuses
200encrypted 400field blank, bad Base64, unusable key, or payload over 190 bytes 500encryption failed
example
curl -s -X POST $BASE/api/v0/cipher/rsa/encrypt \
  -H 'Content-Type: application/json' \
  -d '{"data":"aGVsbG8=","publicKey":"<public-key>"}'
post /api/v0/cipher/rsa/decrypt

Decrypts data produced by the encrypt endpoint using the matching private key.

body — application/json
fieldtypedescription
datastringrequiredBase64 cipher text
privateKeystringrequiredBase64 PKCS#8 RSA private key
response 200
{
  "data": "aGVsbG8="
}
statuses
200decrypted 400field blank, bad Base64, or unusable key 500wrong key or corrupted payload
example
curl -s -X POST $BASE/api/v0/cipher/rsa/decrypt \
  -H 'Content-Type: application/json' \
  -d '{"data":"<cipher>","privateKey":"<private-key>"}'
post /api/v0/cipher/rsa/sign

Signs data with the private key using SHA256withRSA. The payload is hashed first, so there is no size limit — and note that a signature proves authorship without hiding anything: the data itself stays readable.

body — application/json
fieldtypedescription
datastringrequiredBase64 payload to sign
privateKeystringrequiredBase64 PKCS#8 RSA private key
response 200
{
  "signature": "b64-signature"
}
statuses
200signed 400field blank, bad Base64, or unusable key 500signing failed
example
curl -s -X POST $BASE/api/v0/cipher/rsa/sign \
  -H 'Content-Type: application/json' \
  -d '{"data":"aGVsbG8=","privateKey":"<private-key>"}'
post /api/v0/cipher/rsa/verify

Checks a signature against data with the public key. A signature that simply does not match is a normal 200 answer with valid: false — not an error.

body — application/json
fieldtypedescription
datastringrequiredBase64 payload that was signed
signaturestringrequiredBase64 signature from the sign endpoint
publicKeystringrequiredBase64 X.509 RSA public key
response 200
{
  "valid": true
}
statuses
200checked — see valid 400field blank, bad Base64, or unusable key 500verification failed
example
curl -s -X POST $BASE/api/v0/cipher/rsa/verify \
  -H 'Content-Type: application/json' \
  -d '{"data":"aGVsbG8=","signature":"<signature>","publicKey":"<public-key>"}'
04 — cipher / hybrid

RSA cannot encrypt a large file directly, so hybrid encryption does what TLS and PGP do: a fresh single-use AES-256-GCM key encrypts the payload, and RSA-OAEP wraps only that key. Encryption therefore returns two values — data and encryptedKey — and decryption needs both plus the private key. Either half alone is useless.

post /api/v0/cipher/hybrid/encrypt

Encrypts a payload of any size for the holder of the matching RSA private key.

body — application/json
fieldtypedescription
datastringrequiredBase64 payload, no size limit beyond the request body cap
publicKeystringrequiredBase64 X.509 RSA public key
response 200
{
  "data": "b64-aes-gcm-cipher-text",
  "encryptedKey": "b64-rsa-wrapped-aes-key"
}
statuses
200encrypted 400field blank, bad Base64, or unusable key 500encryption failed
example
curl -s -X POST $BASE/api/v0/cipher/hybrid/encrypt \
  -H 'Content-Type: application/json' \
  -d "{\"data\":\"$(base64 -w0 report.pdf)\",\"publicKey\":\"<public-key>\"}"
post /api/v0/cipher/hybrid/decrypt

Unwraps the AES key with the private key and decrypts the payload. The GCM tag is checked, so tampered data fails instead of decoding to garbage.

body — application/json
fieldtypedescription
datastringrequiredBase64 cipher text from the encrypt endpoint
encryptedKeystringrequiredBase64 wrapped AES key from the same response
privateKeystringrequiredBase64 PKCS#8 RSA private key
response 200
{
  "data": "b64-original-payload"
}
statuses
200decrypted 400field blank, bad Base64, or unusable key 500wrong key or corrupted payload
example
curl -s -X POST $BASE/api/v0/cipher/hybrid/decrypt \
  -H 'Content-Type: application/json' \
  -d '{"data":"<cipher>","encryptedKey":"<wrapped-key>","privateKey":"<private-key>"}'
05 — hash

Digests and integrity checks over sha256, sha512, blake3 and hmac-sha256. Algorithm names are matched loosely — SHA-256, sha256 and sha_256 all work — and every response echoes the canonical spelling. Digests come back as lowercase hex. Only hmac-sha256 takes a key; the others ignore one. Hashing is not encryption: a digest proves data has not changed, it does not hide it — see cipher / aes for that.

post /api/v0/hash

Hashes an in-memory payload.

body — application/json
fieldtypedescription
algorithmstringrequiredsha256, sha512, blake3 or hmac-sha256
datastringrequiredthe payload; an empty string is valid and hashes to the empty digest
encodingstringoptionalhow to read data and key: utf8 (default), base64 or hex
keystringoptionalthe HMAC secret — required for hmac-sha256, ignored otherwise
response 200
{
  "algorithm": "sha256",
  "hash": "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",
  "bytes": 5
}
statuses
200hashed 400unknown algorithm, missing data, bad encoding, or HMAC without a key 500hashing failed
example
curl -s -X POST $BASE/api/v0/hash \
  -H 'Content-Type: application/json' \
  -d '{"algorithm":"sha256","data":"hello"}'
example — binary payload
curl -s -X POST $BASE/api/v0/hash \
  -H 'Content-Type: application/json' \
  -d '{"algorithm":"blake3","data":"aGVsbG8=","encoding":"base64"}'
example — hmac
curl -s -X POST $BASE/api/v0/hash \
  -H 'Content-Type: application/json' \
  -d '{"algorithm":"hmac-sha256","data":"hello","key":"secret"}'
post /api/v0/hash/file

Hashes an uploaded file as multipart/form-data. The upload is streamed through the digest rather than buffered, and nothing is stored — this endpoint only computes, unlike uploaded files.

form fields
fieldtypedescription
filefilerequiredthe file itself; exactly one
algorithmstringoptionaldefaults to sha256
keystringoptionalthe HMAC secret — required for hmac-sha256
encodingstringoptionalhow to read key; the file is always raw bytes
response 200
{
  "algorithm": "sha256",
  "hash": "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",
  "bytes": 5,
  "name": "notes.txt"
}
statuses
200hashed 400no file, unknown algorithm, or HMAC without a key 500could not read the upload
example
curl -s -X POST $BASE/api/v0/hash/file \
  -F '[email protected]' \
  -F 'algorithm=blake3'
post /api/v0/hash/verify

Recomputes the digest of data and compares it with the one you expect. The comparison is constant-time, so repeated guesses cannot be timed to recover a secret HMAC. A digest that does not match is a normal 200 with valid: false — not an error.

body — application/json
fieldtypedescription
algorithmstringrequiredthe algorithm the expected hash was made with
datastringrequiredthe payload to check
hashstringrequiredthe expected digest as hex; case and surrounding spaces are ignored
encodingstringoptionalutf8 (default), base64 or hex
keystringoptionalthe HMAC secret — required for hmac-sha256
response 200
{
  "valid": true,
  "algorithm": "sha256",
  "expected": "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",
  "actual":   "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
}
statuses
200checked — see valid 400unknown algorithm, missing data, or a hash that is not hex 500verification failed
example
curl -s -X POST $BASE/api/v0/hash/verify \
  -H 'Content-Type: application/json' \
  -d '{"algorithm":"sha256","data":"hello",
       "hash":"2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"}'
06 — qr

Renders any text as a QR code, and reads one back out of an image. Nothing is stored — a code is built per request and handed straight back. png is the one to paste into a document, svg the one to print at any size. A payload is capped at 2953 bytes, which is all a QR code can hold at all. To shorten a long url and get its code in one call, see post /api/v0/short-url/qr.

post /api/v0/qr/generate

Renders data as a QR code. The response is the image itself, so it can be saved straight to a file. Send Accept: application/json (or add ?json=true) to get a data: URL in JSON instead — that is the form to drop into an <img src>.

body — application/json
fieldtypedescription
datastringrequiredthe text to encode — a url, a wifi string, anything; up to 2953 bytes
formatstringoptionalpng (default) or svg
sizenumberoptionalimage edge in pixels, 64–4096; default 512
errorCorrectionstringoptionalL ~7%, M ~15% (default), Q ~25%, H ~30% of the code may be damaged and still read
foregroundstringoptionalmodule colour as #rgb, #rrggbb or #rrggbbaa; default #000000
backgroundstringoptionalbackdrop colour, same notation; default #ffffff. #00000000 leaves it out entirely
marginnumberoptionalquiet zone in modules, 0–32; default 4, which is what the spec asks for
response 200 — image
Content-Type: image/png
<the png bytes>
response 200 — application/json
{
  "format": "png",
  "contentType": "image/png",
  "size": 512,
  "image": "data:image/png;base64,iVBORw0KGgo…"
}
statuses
200rendered 400missing data, a payload too long to encode, or an option out of range 500rendering failed
example — save a png
curl -s -X POST $BASE/api/v0/qr/generate \
  -H 'Content-Type: application/json' \
  -d '{"data":"https://ss.serbekun.com","size":512}' -o qr.png
example — svg, high redundancy, coloured
curl -s -X POST $BASE/api/v0/qr/generate \
  -H 'Content-Type: application/json' \
  -d '{"data":"https://ss.serbekun.com","format":"svg",
       "errorCorrection":"H","foreground":"#123456","background":"#00000000"}' -o qr.svg
example — as a data url
curl -s -X POST $BASE/api/v0/qr/generate \
  -H 'Content-Type: application/json' -H 'Accept: application/json' \
  -d '{"data":"https://ss.serbekun.com"}'
post /api/v0/qr/read

Reads the code out of an image — a screenshot, a photo, a downloaded png. Codes printed light-on-dark are read too. An image that simply holds no code is a 200 with found: false, not an error. SVG cannot be read; rasterise it first.

body — any one of three
fieldindescription
filemultipartone ofthe image as an upload
raw bodyone ofthe image bytes with an image/* content type
imagejsonone ofthe image as base64, with or without a data: prefix
response 200
{
  "found": true,
  "text": "https://ss.serbekun.com",
  "format": "QR_CODE"
}
statuses
200read, or nothing found 400no image sent, bad base64, or bytes that are not a readable image 500reading failed
example — an uploaded file
curl -s -X POST $BASE/api/v0/qr/read -F '[email protected]'
example — raw bytes
curl -s -X POST $BASE/api/v0/qr/read \
  -H 'Content-Type: image/png' --data-binary '@qr.png'
07 — encoding

The same bytes written another way — base64, base64url, base32, hex, url percent-encoding and plain utf8 text. Every route here is one conversion: the input is decoded out of the format it came in, and those bytes are written into the format asked for. Nothing is hidden or protected by any of it — for that see cipher, and for proving data has not changed see hash. Format names are matched loosely (BASE-64, b64, base16, text) and every response echoes the canonical spelling. bytes is always the size of the payload itself, not of the text it is written as.

post /api/v0/encoding/{format}/encode

Writes a payload as base64, hex or url — the format is the one in the path. encoding says how to read the input, so binary can be handed in as hex or base64 rather than as text.

path params
paramtypedescription
formatstringrequiredbase64, hex or url
body — application/json
fieldtypedescription
datastringrequiredthe payload; an empty string is valid
encodingstringoptionalhow to read data: utf8 (default), base64, base64url, base32, hex, url, url-form
formbooleanoptionalon /url/encode only — true writes x-www-form-urlencoded (a space becomes +) instead of RFC 3986
response 200
{
  "data": "Zm9vYmFy",
  "bytes": 6,
  "from": "utf8",
  "to": "base64"
}
statuses
200converted 400missing data, an unknown encoding, or input that is not valid in it
example
curl -s -X POST $BASE/api/v0/encoding/base64/encode \
  -H 'Content-Type: application/json' \
  -d '{"data":"foobar"}'
example — binary in, hex out
curl -s -X POST $BASE/api/v0/encoding/hex/encode \
  -H 'Content-Type: application/json' \
  -d '{"data":"Zm9vYmFy","encoding":"base64"}'
example — form-style url encoding
curl -s -X POST $BASE/api/v0/encoding/url/encode \
  -H 'Content-Type: application/json' \
  -d '{"data":"a b+c","form":true}'
post /api/v0/encoding/{format}/decode

Reads a payload written as base64, hex or url. outputEncoding says how to write the result — it defaults to utf8, and bytes that are not valid UTF-8 are refused rather than mangled into , so binary needs hex or base64 here. Decoding is forgiving about how the input was pasted: whitespace, lowercase, missing padding, a 0x prefix and : separators in hex, and the url-safe alphabet on the base64 route.

path params
paramtypedescription
formatstringrequiredbase64, hex or url
body — application/json
fieldtypedescription
datastringrequiredthe encoded payload
outputEncodingstringoptionalhow to write the result: utf8 (default), base64, base64url, base32, hex, url, url-form
formbooleanoptionalon /url/decode only — true reads x-www-form-urlencoded, where + is a space
response 200
{
  "data": "foobar",
  "bytes": 6,
  "from": "base64",
  "to": "utf8"
}
statuses
200converted 400input that is not valid in the format, or bytes that are not text when text was asked for
example
curl -s -X POST $BASE/api/v0/encoding/base64/decode \
  -H 'Content-Type: application/json' \
  -d '{"data":"Zm9vYmFy"}'
example — binary payload
curl -s -X POST $BASE/api/v0/encoding/base64/decode \
  -H 'Content-Type: application/json' \
  -d '{"data":"//79","outputEncoding":"hex"}'
post /api/v0/encoding/convert

The general form: name both sides. The six routes above are this one with a side pinned by the path.

body — application/json
fieldtypedescription
datastringrequiredthe payload as written in from
fromstringrequiredutf8, base64, base64url, base32, hex, url or url-form
tostringrequiredthe format to write the result in, same set
response 200
{
  "data": "MZXW6YTBOI======",
  "bytes": 6,
  "from": "base64",
  "to": "base32"
}
statuses
200converted 400missing data, an unknown format, or input that is not valid in from
example
curl -s -X POST $BASE/api/v0/encoding/convert \
  -H 'Content-Type: application/json' \
  -d '{"data":"Zm9vYmFy","from":"base64","to":"base32"}'
08 — id & random

Identifiers and random material, all of it drawn from a SecureRandom — these values are used as session tokens and delete keys, and a predictable generator would make every one of them guessable. Nothing is stored, so the same request never returns the same answer twice. Values always come back as an array, even when one was asked for, so a client never has to branch on the count. bits is how much randomness a single value carries, rounded down — the number that actually says how hard one is to guess. count is capped at 1000 per call.

get /api/v0/id/uuid

Generates UUIDs. v4 is 122 random bits and reveals nothing; v7 puts a millisecond timestamp in the leading 48 bits, so ids sort by creation time — far kinder to a database index, at the cost of telling anyone who reads one when it was made.

query params
paramtypedescription
countnumberoptional1–1000; default 1
versionstringoptionalv4 (default) or v7
formatstringoptionalcanonical (default), compact (no hyphens), upper, urn
response 200
{
  "type": "uuid",
  "count": 2,
  "values": ["7f4b…", "0c19…"],
  "format": "canonical",
  "version": "v4",
  "bits": 122
}
statuses
200generated 400count out of range or not a number, or an unknown version or format
example
curl -s "$BASE/api/v0/id/uuid?count=5&version=v7&format=compact"
get /api/v0/id/ulid

Generates ULIDs — 128 bits, a millisecond timestamp then 80 random, written as 26 characters of Crockford base32 (no I, L, O or U, so one cannot be misread aloud). They sort by time as plain text, and ids made inside the same millisecond still ascend, because the random half is stepped rather than redrawn.

query params
paramtypedescription
countnumberoptional1–1000; default 1
formatstringoptionalcanonical (default), lower, uuid — the same bits as a UUID string, which fits a uuid column exactly, though it is not an RFC 4122 UUID: the version and variant nibbles carry ULID randomness — or hex
response 200
{
  "type": "ulid",
  "count": 1,
  "values": ["01JD3K9Q7WZ8XN4M2B6R0YFVTA"],
  "format": "canonical",
  "bits": 80
}
statuses
200generated 400count out of range, or an unknown format
example
curl -s "$BASE/api/v0/id/ulid?count=10"
get /api/v0/random/token

Random strings drawn from an alphabet — api keys, session tokens, invite codes. base58 drops the characters that look alike (0OIl) and is the one to pick if a person will ever read a token off a screen; base64url packs the most entropy per character into something still safe in a url.

query params
paramtypedescription
countnumberoptional1–1000; default 1
lengthnumberoptionalcharacters, 1–4096; default 32
alphabetstringoptionalbase62 (default), base58, base64url, base32, hex, digits, lower, upper
charsstringoptionalyour own set of 2–256 characters, used instead of alphabet; a repeated character is refused, since it would be drawn twice as often and the reported bits would lie
response 200
{
  "type": "token",
  "count": 1,
  "values": ["nQ2rXk…"],
  "alphabet": "base62",
  "length": 32,
  "bits": 190
}
statuses
200generated 400count or length out of range, an unknown alphabet, or a custom set that is too small or repeats
example
curl -s "$BASE/api/v0/random/token?length=24&alphabet=base58"
example — a six digit code
curl -s "$BASE/api/v0/random/token?length=6&alphabet=digits"
get /api/v0/random/bytes

Raw randomness written however you need it — a key, a salt, an iv. The bytes are the same in every format; utf8 is refused, because random bytes are not text.

query params
paramtypedescription
countnumberoptionalhow many separate values, 1–1000; default 1
lengthnumberoptionalbytes per value, 1–4096; default 32
formatstringoptionalhex (default), base64, base64url, base32
response 200
{
  "type": "bytes",
  "count": 1,
  "values": ["9f86d081884c7d65…"],
  "format": "hex",
  "length": 32,
  "bits": 256
}
statuses
200generated 400count or length out of range, or format: utf8
example — a 256 bit key
curl -s "$BASE/api/v0/random/bytes?length=32&format=base64"
post /api/v0/id/batch

Every kind above in one round trip — each item takes exactly the parameters its own endpoint does. A body that is a single item on its own is accepted too, without the items array around it. The 1000 cap is on the whole call, not on each item.

body — application/json
fieldtypedescription
itemsarrayrequiredthe requests, in order
items[].typestringrequireduuid, ulid, token or bytes
items[].countnumberoptionaldefault 1
items[].version, format, length, alphabet, charsoptionalas on the matching endpoint above
response 200
{
  "items": [
    {"type": "uuid", "count": 2, "values": ["…", "…"], "format": "canonical", "version": "v7", "bits": 74},
    {"type": "token", "count": 1, "values": ["…"], "alphabet": "base58", "length": 20, "bits": 117}
  ]
}
statuses
200generated 400no items, an unknown type, a parameter out of range, or more than 1000 values in total
example
curl -s -X POST $BASE/api/v0/id/batch \
  -H 'Content-Type: application/json' \
  -d '{"items":[{"type":"uuid","count":2,"version":"v7"},
                {"type":"token","length":20,"alphabet":"base58"}]}'
09 — json

Built for a shell: the request body is the document itself, not a wrapper around it, and the options ride in the query string — so curl --data-binary @file.json just works. It is also the only shape that can take a broken document at all, since invalid JSON cannot be quoted inside a JSON envelope. diff is the exception: it needs two documents, so it takes {"from": …, "to": …}. format and minify answer with the document itself for the same reason — the output of a formatter is a JSON document, and you should not have to unescape it out of a field.

Two parsing rules run through all five: duplicate keys are an error (a lenient parser keeps the last one, which would make the formatter delete data while reporting success), and numbers keep their value exactly — decimals are read as arbitrary precision, so a long fraction survives a round trip that a double would round off. The text may still be normalised: 1e2 comes back as 1E+2.

post /api/v0/json/validate

Says whether the body is JSON, and where it stops being JSON if it is not. A document that does not parse is a 200 with valid: false — telling you which line the comma is missing from is the whole job, not a failure of it.

body

The document, exactly as it is. Anything at all — it does not have to be valid.

response 200 — valid
{"valid": true, "bytes": 16}
response 200 — not valid
{
  "valid": false,
  "error": "Unexpected character ('}' (code 125)): expected a valid value",
  "line": 4,
  "column": 2,
  "bytes": 20
}
statuses
200checked — valid or not
example
curl -s -X POST $BASE/api/v0/json/validate --data-binary @file.json
post /api/v0/json/format

Re-writes the document with indentation, and answers with the document — not with JSON about the document. Sorting the keys is what makes two files comparable line by line.

query params
paramtypedescription
indentstringoptionalspaces, 1–16, or tab; default 2
sortbooleanoptionalorder every object's keys, at every depth. Array order is data and is never touched
body

The document.

response 200 — application/json
{
  "a": 1,
  "b": [
    1,
    2
  ]
}
statuses
200formatted 400the document does not parse (with the line and column), or the indent is not a width this can write
example
curl -s -X POST "$BASE/api/v0/json/format?indent=4&sort=true" \
  --data-binary @file.json
post /api/v0/json/minify

Strips every byte that is not part of the value, and answers with the document.

query params
paramtypedescription
sortbooleanoptionalorder every object's keys — with this, minify is a canonical form
response 200 — application/json
{"a":1,"b":[1,2]}
statuses
200minified 400the document does not parse
example
curl -s -X POST $BASE/api/v0/json/minify --data-binary @file.json
post /api/v0/json/query

Pulls values out of the document. A JSON Pointer (RFC 6901) names one place and finds at most one value — and is the same syntax a patch uses, so a pointer that finds a value is the pointer that would change it. A JSONPath can wildcard, slice and filter, and finds as many as match. Finding nothing is a 200 with count: 0, since "is there anything here" is a fair question.

query params
paramtypedescription
pointerstringone ofa JSON Pointer, e.g. /store/book/0/title; names its own syntax
pathstringone ofa JSONPath, e.g. $..book[?(@.price < 10)]; names its own syntax
expressionstringone ofeither one — the syntax is worked out from it (only a JSONPath starts with $)
syntaxstringoptionalpointer or jsonpath, when you would rather say than have it guessed
response 200
{
  "expression": "$..book[?(@.price < 10)].title",
  "syntax": "jsonpath",
  "count": 1,
  "matches": ["Moby Dick"],
  "paths": ["$['store']['book'][0]['title']"]
}
statuses
200searched — matches or none 400no expression, the document does not parse, or the expression is not valid in its syntax
example — pointer
curl -s -X POST "$BASE/api/v0/json/query?pointer=/store/book/0/title" \
  --data-binary @file.json
example — jsonpath with a filter
curl -s -X POST "$BASE/api/v0/json/query" \
  --data-urlencode 'path=$..book[?(@.price < 10)].title' -G --data-binary @file.json
post /api/v0/json/diff

Answers with an RFC 6902 JSON Patch: the operations that turn from into to. Only add, remove and replace are emitted — move and copy are optional in the RFC and only ever shorten a patch. Numbers compare by value, so 1 and 1.0 are not a change. Arrays of the same length are compared element by element, so a change deep inside one element is one nested operation; when the lengths differ, a longest-common-subsequence match works out what was really inserted or dropped, so adding one element to the front of a thousand is one operation, not a thousand.

body — application/json
fieldtypedescription
fromanyrequiredthe document as it is
toanyrequiredthe document as it should be
response 200
{
  "equal": false,
  "operations": 3,
  "patch": [
    {"op": "remove", "path": "/gone"},
    {"op": "replace", "path": "/a", "value": 2},
    {"op": "add", "path": "/list/1", "value": 9}
  ]
}
statuses
200compared 400a missing side, or a body that is not JSON
example
curl -s -X POST $BASE/api/v0/json/diff \
  -H 'Content-Type: application/json' \
  -d '{"from":{"a":1,"gone":true},"to":{"a":2}}'
10 — short url

The delete token is returned only once, at creation time. Without it a short url cannot be removed.

post /api/v0/short-url

Creates a short url pointing at url.

body — application/json
fieldtypedescription
urlstringrequiredtarget url
namestringoptionalhuman readable label
descriptionstringoptionalfree form note
response 201
{
  "id": "aB3xY",
  "token": "8f14e45f-…"
}
statuses
201created 400url missing or invalid
example
curl -s -X POST $BASE/api/v0/short-url \
  -H 'Content-Type: application/json' \
  -d '{"url":"https://example.com"}'
post /api/v0/short-url/qr

Creates a short url and renders the short link as a QR code in one round trip — the pair to print on a poster, where the original url would be both unscannable and unreadable. Takes everything post /api/v0/qr/generate takes, plus the fields of a plain creation.

body — application/json
fieldtypedescription
urlstringrequiredtarget url
namestringoptionalhuman readable label
descriptionstringoptionalfree form note
baseUrlstringoptionalthe origin the code should point at; derived from the request host (and X-Forwarded-*) when absent
format, size, errorCorrection, foreground, background, marginoptionalas in qr / generate
response 201
{
  "id": "aB3xY",
  "token": "8f14e45f-…",
  "shortUrl": "https://ss.serbekun.com/api/v0/short-url/aB3xY",
  "format": "png",
  "contentType": "image/png",
  "size": 512,
  "qr": "data:image/png;base64,iVBORw0KGgo…"
}
statuses
201created 400url missing or invalid, a bad baseUrl, or a QR option out of range 500rendering failed
example
curl -s -X POST $BASE/api/v0/short-url/qr \
  -H 'Content-Type: application/json' \
  -d '{"url":"https://example.com/a/very/long/path","size":640}'
get /api/v0/short-url/{id}

Resolves a short url and redirects to the target. This is the link you share.

path params
paramtypedescription
idstringrequiredshort id from creation
statuses
302redirect to target url 400id missing 404unknown id
example
curl -sI $BASE/api/v0/short-url/aB3xY
del /api/v0/short-url/{id}

Deletes a short url. The token may be passed as a query param or in the JSON body.

params
paramindescription
idpathrequiredshort id
tokenquery / bodyrequireddelete token from creation
statuses
204deleted 403token mismatch 404unknown id
example
curl -s -X DELETE "$BASE/api/v0/short-url/aB3xY?token=<token>"
12 — uploaded files

Files are stored with an optional TTL and are swept by a background cleanup task once expired. Every read and delete needs the token handed out at upload time.

post /api/v0/uploaded-files

Uploads a single file as multipart/form-data.

form fields
fieldtypedescription
filefilerequiredthe file itself; exactly one
namestringoptionaldisplay name, defaults to the original filename
ttlnumberoptionallifetime in seconds; 0 or absent means no expiry
response 201
{
  "uuid": "7c2d…-uuid",
  "token": "4e5f…-uuid",
  "name": "report.pdf",
  "expiredTime": 1785000000000
}
statuses
201uploaded 400no file in the request 413over the configured size limit 500failed to store the file
example
curl -s -X POST $BASE/api/v0/uploaded-files \
  -F '[email protected]' \
  -F 'name=report.pdf' \
  -F 'ttl=3600'
get /api/v0/uploaded-files/max-size

Returns the configured upload limit, so clients can reject oversized files before sending them.

response 200
{
  "megabytes": 100,
  "bytes": 104857600
}
example
curl -s $BASE/api/v0/uploaded-files/max-size
get /api/v0/uploaded-files/{uuid}

Metadata of one file. The token is never included in the response.

params
paramindescription
uuidpathrequiredfile uuid
tokenqueryrequiredfile access token
response 200
{
  "uuid": "7c2d…-uuid",
  "name": "report.pdf",
  "expiredTime": 1785000000000
}
statuses
200found 400malformed uuid 403invalid or missing token 404unknown or expired file
example
curl -s "$BASE/api/v0/uploaded-files/<uuid>?token=<token>"
get /api/v0/uploaded-files/{uuid}/download

Streams the file content as application/octet-stream with a Content-Disposition attachment header.

params
paramindescription
uuidpathrequiredfile uuid
tokenqueryrequiredfile access token
statuses
200file bytes 403invalid or missing token 404unknown, expired, or missing on disk 500read error
example
curl -sOJ "$BASE/api/v0/uploaded-files/<uuid>/download?token=<token>"
del /api/v0/uploaded-files/{uuid}

Deletes the file and its metadata. The token may be a query param or a JSON body field.

params
paramindescription
uuidpathrequiredfile uuid
tokenquery / bodyrequiredfile access token
statuses
204deleted 403token mismatch 404unknown file 500failed to delete from disk
example
curl -s -X DELETE "$BASE/api/v0/uploaded-files/<uuid>?token=<token>"
get /api/v0/uploaded-files

Listing every uploaded file is deliberately not allowed — the route exists but always refuses. Files are reachable only by uuid plus token.

response 403
{
  "error": "Listing all files is not allowed"
}
14 — youtube

Backed by yt-dlp on the server. Playlist parameters are ignored, age-restricted videos need server-side cookies, and each call is capped at a 120 second timeout.

get /api/v0/youtube/info

Returns the raw yt-dlp metadata JSON for a video.

query params
paramtypedescription
urlstringrequiredvideo url
statuses
200metadata json 400url missing or rejected 500yt-dlp failure
example
curl -s "$BASE/api/v0/youtube/info?url=https://youtu.be/<id>"
get /api/v0/youtube/download

Downloads the video on the server and returns the MP4 bytes (video/mp4).

query params
paramtypedescription
urlstringrequiredvideo url
statuses
200mp4 bytes 400url missing or rejected 500yt-dlp failure
example
curl -s -o video.mp4 "$BASE/api/v0/youtube/download?url=https://youtu.be/<id>"
15 — network
get /api/v0/network/ip

Returns the caller's ip as the server sees it. Behind a proxy the value comes from CF-Connecting-IP, then X-Forwarded-For, then the socket address — good enough to show users their own ip, not for auth or rate limiting.

response 200
{
  "ip": "203.0.113.7"
}
example
curl -s $BASE/api/v0/network/ip
16 — static

Classpath resources served straight from the jar. Every category behaves the same way: without a name it lists what is available, with a name it returns the file.

get /

The index page listing every service.

statuses
200html 404index resource missing
get /icon

The site favicon (ss_icon.svg).

get /static/v0/{category}

Lists the files available in a category.

categories
categorycontent
htmlservice pages
cssstylesheets
jsclient scripts
imagesraster images
svgvector images
jsonstatic datasets
pdfdocuments
domaindomain files
example
curl -s $BASE/static/v0/json
get /static/v0/{category}/{name}

Returns one static file with the content type of its category.

statuses
200file content 404no such resource
example
curl -s $BASE/static/v0/html/api_docs.html