// every endpoint of the server, v0
no endpoints match the filter.
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.
Returns the build version of the running server.
{
"version": "1.4.0"
}
curl -s $BASE/api/v0/version
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.
Generates a fresh AES key. The key is returned once and never persisted.
{
"key": "b64-encoded-aes-key"
}
curl -s $BASE/api/v0/cipher/aes
Encrypts data with the given AES key.
| field | type | description | |
|---|---|---|---|
| data | string | required | plain text to encrypt |
| key | string | required | Base64 AES key |
{
"data": "b64-cipher-text"
}
data or key blank
500encryption failed
curl -s -X POST $BASE/api/v0/cipher/aes/encrypt \
-H 'Content-Type: application/json' \
-d '{"data":"hello","key":"<key>"}'
Decrypts data produced by the encrypt endpoint using the same key.
| field | type | description | |
|---|---|---|---|
| data | string | required | Base64 cipher text |
| key | string | required | the same Base64 AES key |
{
"data": "hello"
}
data or key blank
500wrong key or corrupted payload
curl -s -X POST $BASE/api/v0/cipher/aes/decrypt \
-H 'Content-Type: application/json' \
-d '{"data":"<cipher>","key":"<key>"}'
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.
Generates a fresh 2048-bit RSA key pair. Both keys are returned once and never persisted.
{
"publicKey": "b64-x509-public-key",
"privateKey": "b64-pkcs8-private-key"
}
curl -s $BASE/api/v0/cipher/rsa/keypair
Encrypts data with an RSA publicKey. Only the holder of the matching private key can read the result.
| field | type | description | |
|---|---|---|---|
| data | string | required | Base64 payload, at most 190 bytes decoded |
| publicKey | string | required | Base64 X.509 RSA public key |
{
"data": "b64-cipher-text"
}
curl -s -X POST $BASE/api/v0/cipher/rsa/encrypt \
-H 'Content-Type: application/json' \
-d '{"data":"aGVsbG8=","publicKey":"<public-key>"}'
Decrypts data produced by the encrypt endpoint using the matching private key.
| field | type | description | |
|---|---|---|---|
| data | string | required | Base64 cipher text |
| privateKey | string | required | Base64 PKCS#8 RSA private key |
{
"data": "aGVsbG8="
}
curl -s -X POST $BASE/api/v0/cipher/rsa/decrypt \
-H 'Content-Type: application/json' \
-d '{"data":"<cipher>","privateKey":"<private-key>"}'
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.
| field | type | description | |
|---|---|---|---|
| data | string | required | Base64 payload to sign |
| privateKey | string | required | Base64 PKCS#8 RSA private key |
{
"signature": "b64-signature"
}
curl -s -X POST $BASE/api/v0/cipher/rsa/sign \
-H 'Content-Type: application/json' \
-d '{"data":"aGVsbG8=","privateKey":"<private-key>"}'
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.
| field | type | description | |
|---|---|---|---|
| data | string | required | Base64 payload that was signed |
| signature | string | required | Base64 signature from the sign endpoint |
| publicKey | string | required | Base64 X.509 RSA public key |
{
"valid": true
}
valid
400field blank, bad Base64, or unusable key
500verification failed
curl -s -X POST $BASE/api/v0/cipher/rsa/verify \
-H 'Content-Type: application/json' \
-d '{"data":"aGVsbG8=","signature":"<signature>","publicKey":"<public-key>"}'
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.
Encrypts a payload of any size for the holder of the matching RSA private key.
| field | type | description | |
|---|---|---|---|
| data | string | required | Base64 payload, no size limit beyond the request body cap |
| publicKey | string | required | Base64 X.509 RSA public key |
{
"data": "b64-aes-gcm-cipher-text",
"encryptedKey": "b64-rsa-wrapped-aes-key"
}
curl -s -X POST $BASE/api/v0/cipher/hybrid/encrypt \
-H 'Content-Type: application/json' \
-d "{\"data\":\"$(base64 -w0 report.pdf)\",\"publicKey\":\"<public-key>\"}"
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.
| field | type | description | |
|---|---|---|---|
| data | string | required | Base64 cipher text from the encrypt endpoint |
| encryptedKey | string | required | Base64 wrapped AES key from the same response |
| privateKey | string | required | Base64 PKCS#8 RSA private key |
{
"data": "b64-original-payload"
}
curl -s -X POST $BASE/api/v0/cipher/hybrid/decrypt \
-H 'Content-Type: application/json' \
-d '{"data":"<cipher>","encryptedKey":"<wrapped-key>","privateKey":"<private-key>"}'
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.
Hashes an in-memory payload.
| field | type | description | |
|---|---|---|---|
| algorithm | string | required | sha256, sha512, blake3 or hmac-sha256 |
| data | string | required | the payload; an empty string is valid and hashes to the empty digest |
| encoding | string | optional | how to read data and key: utf8 (default), base64 or hex |
| key | string | optional | the HMAC secret — required for hmac-sha256, ignored otherwise |
{
"algorithm": "sha256",
"hash": "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",
"bytes": 5
}
data, bad encoding, or HMAC without a key
500hashing failed
curl -s -X POST $BASE/api/v0/hash \
-H 'Content-Type: application/json' \
-d '{"algorithm":"sha256","data":"hello"}'
curl -s -X POST $BASE/api/v0/hash \
-H 'Content-Type: application/json' \
-d '{"algorithm":"blake3","data":"aGVsbG8=","encoding":"base64"}'
curl -s -X POST $BASE/api/v0/hash \
-H 'Content-Type: application/json' \
-d '{"algorithm":"hmac-sha256","data":"hello","key":"secret"}'
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.
| field | type | description | |
|---|---|---|---|
| file | file | required | the file itself; exactly one |
| algorithm | string | optional | defaults to sha256 |
| key | string | optional | the HMAC secret — required for hmac-sha256 |
| encoding | string | optional | how to read key; the file is always raw bytes |
{
"algorithm": "sha256",
"hash": "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",
"bytes": 5,
"name": "notes.txt"
}
curl -s -X POST $BASE/api/v0/hash/file \ -F '[email protected]' \ -F 'algorithm=blake3'
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.
| field | type | description | |
|---|---|---|---|
| algorithm | string | required | the algorithm the expected hash was made with |
| data | string | required | the payload to check |
| hash | string | required | the expected digest as hex; case and surrounding spaces are ignored |
| encoding | string | optional | utf8 (default), base64 or hex |
| key | string | optional | the HMAC secret — required for hmac-sha256 |
{
"valid": true,
"algorithm": "sha256",
"expected": "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",
"actual": "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
}
valid
400unknown algorithm, missing data, or a hash that is not hex
500verification failed
curl -s -X POST $BASE/api/v0/hash/verify \
-H 'Content-Type: application/json' \
-d '{"algorithm":"sha256","data":"hello",
"hash":"2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"}'
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.
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>.
| field | type | description | |
|---|---|---|---|
| data | string | required | the text to encode — a url, a wifi string, anything; up to 2953 bytes |
| format | string | optional | png (default) or svg |
| size | number | optional | image edge in pixels, 64–4096; default 512 |
| errorCorrection | string | optional | L ~7%, M ~15% (default), Q ~25%, H ~30% of the code may be damaged and still read |
| foreground | string | optional | module colour as #rgb, #rrggbb or #rrggbbaa; default #000000 |
| background | string | optional | backdrop colour, same notation; default #ffffff. #00000000 leaves it out entirely |
| margin | number | optional | quiet zone in modules, 0–32; default 4, which is what the spec asks for |
Content-Type: image/png <the png bytes>
{
"format": "png",
"contentType": "image/png",
"size": 512,
"image": "data:image/png;base64,iVBORw0KGgo…"
}
data, a payload too long to encode, or an option out of range
500rendering failed
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
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
curl -s -X POST $BASE/api/v0/qr/generate \
-H 'Content-Type: application/json' -H 'Accept: application/json' \
-d '{"data":"https://ss.serbekun.com"}'
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.
| field | in | description | |
|---|---|---|---|
| file | multipart | one of | the image as an upload |
| — | raw body | one of | the image bytes with an image/* content type |
| image | json | one of | the image as base64, with or without a data: prefix |
{
"found": true,
"text": "https://ss.serbekun.com",
"format": "QR_CODE"
}
curl -s -X POST $BASE/api/v0/qr/read -F '[email protected]'
curl -s -X POST $BASE/api/v0/qr/read \ -H 'Content-Type: image/png' --data-binary '@qr.png'
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.
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.
| param | type | description | |
|---|---|---|---|
| format | string | required | base64, hex or url |
| field | type | description | |
|---|---|---|---|
| data | string | required | the payload; an empty string is valid |
| encoding | string | optional | how to read data: utf8 (default), base64, base64url, base32, hex, url, url-form |
| form | boolean | optional | on /url/encode only — true writes x-www-form-urlencoded (a space becomes +) instead of RFC 3986 |
{
"data": "Zm9vYmFy",
"bytes": 6,
"from": "utf8",
"to": "base64"
}
data, an unknown encoding, or input that is not valid in it
curl -s -X POST $BASE/api/v0/encoding/base64/encode \
-H 'Content-Type: application/json' \
-d '{"data":"foobar"}'
curl -s -X POST $BASE/api/v0/encoding/hex/encode \
-H 'Content-Type: application/json' \
-d '{"data":"Zm9vYmFy","encoding":"base64"}'
curl -s -X POST $BASE/api/v0/encoding/url/encode \
-H 'Content-Type: application/json' \
-d '{"data":"a b+c","form":true}'
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.
| param | type | description | |
|---|---|---|---|
| format | string | required | base64, hex or url |
| field | type | description | |
|---|---|---|---|
| data | string | required | the encoded payload |
| outputEncoding | string | optional | how to write the result: utf8 (default), base64, base64url, base32, hex, url, url-form |
| form | boolean | optional | on /url/decode only — true reads x-www-form-urlencoded, where + is a space |
{
"data": "foobar",
"bytes": 6,
"from": "base64",
"to": "utf8"
}
curl -s -X POST $BASE/api/v0/encoding/base64/decode \
-H 'Content-Type: application/json' \
-d '{"data":"Zm9vYmFy"}'
curl -s -X POST $BASE/api/v0/encoding/base64/decode \
-H 'Content-Type: application/json' \
-d '{"data":"//79","outputEncoding":"hex"}'
The general form: name both sides. The six routes above are this one with a side pinned by the path.
| field | type | description | |
|---|---|---|---|
| data | string | required | the payload as written in from |
| from | string | required | utf8, base64, base64url, base32, hex, url or url-form |
| to | string | required | the format to write the result in, same set |
{
"data": "MZXW6YTBOI======",
"bytes": 6,
"from": "base64",
"to": "base32"
}
data, an unknown format, or input that is not valid in from
curl -s -X POST $BASE/api/v0/encoding/convert \
-H 'Content-Type: application/json' \
-d '{"data":"Zm9vYmFy","from":"base64","to":"base32"}'
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.
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.
| param | type | description | |
|---|---|---|---|
| count | number | optional | 1–1000; default 1 |
| version | string | optional | v4 (default) or v7 |
| format | string | optional | canonical (default), compact (no hyphens), upper, urn |
{
"type": "uuid",
"count": 2,
"values": ["7f4b…", "0c19…"],
"format": "canonical",
"version": "v4",
"bits": 122
}
count out of range or not a number, or an unknown version or format
curl -s "$BASE/api/v0/id/uuid?count=5&version=v7&format=compact"
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.
| param | type | description | |
|---|---|---|---|
| count | number | optional | 1–1000; default 1 |
| format | string | optional | canonical (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 |
{
"type": "ulid",
"count": 1,
"values": ["01JD3K9Q7WZ8XN4M2B6R0YFVTA"],
"format": "canonical",
"bits": 80
}
count out of range, or an unknown format
curl -s "$BASE/api/v0/id/ulid?count=10"
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.
| param | type | description | |
|---|---|---|---|
| count | number | optional | 1–1000; default 1 |
| length | number | optional | characters, 1–4096; default 32 |
| alphabet | string | optional | base62 (default), base58, base64url, base32, hex, digits, lower, upper |
| chars | string | optional | your 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 |
{
"type": "token",
"count": 1,
"values": ["nQ2rXk…"],
"alphabet": "base62",
"length": 32,
"bits": 190
}
count or length out of range, an unknown alphabet, or a custom set that is too small or repeats
curl -s "$BASE/api/v0/random/token?length=24&alphabet=base58"
curl -s "$BASE/api/v0/random/token?length=6&alphabet=digits"
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.
| param | type | description | |
|---|---|---|---|
| count | number | optional | how many separate values, 1–1000; default 1 |
| length | number | optional | bytes per value, 1–4096; default 32 |
| format | string | optional | hex (default), base64, base64url, base32 |
{
"type": "bytes",
"count": 1,
"values": ["9f86d081884c7d65…"],
"format": "hex",
"length": 32,
"bits": 256
}
count or length out of range, or format: utf8
curl -s "$BASE/api/v0/random/bytes?length=32&format=base64"
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.
| field | type | description | |
|---|---|---|---|
| items | array | required | the requests, in order |
| items[].type | string | required | uuid, ulid, token or bytes |
| items[].count | number | optional | default 1 |
| items[].version, format, length, alphabet, chars | optional | as on the matching endpoint above |
{
"items": [
{"type": "uuid", "count": 2, "values": ["…", "…"], "format": "canonical", "version": "v7", "bits": 74},
{"type": "token", "count": 1, "values": ["…"], "alphabet": "base58", "length": 20, "bits": 117}
]
}
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"}]}'
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.
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.
The document, exactly as it is. Anything at all — it does not have to be valid.
{"valid": true, "bytes": 16}
{
"valid": false,
"error": "Unexpected character ('}' (code 125)): expected a valid value",
"line": 4,
"column": 2,
"bytes": 20
}
curl -s -X POST $BASE/api/v0/json/validate --data-binary @file.json
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.
| param | type | description | |
|---|---|---|---|
| indent | string | optional | spaces, 1–16, or tab; default 2 |
| sort | boolean | optional | order every object's keys, at every depth. Array order is data and is never touched |
The document.
{
"a": 1,
"b": [
1,
2
]
}
curl -s -X POST "$BASE/api/v0/json/format?indent=4&sort=true" \ --data-binary @file.json
Strips every byte that is not part of the value, and answers with the document.
| param | type | description | |
|---|---|---|---|
| sort | boolean | optional | order every object's keys — with this, minify is a canonical form |
{"a":1,"b":[1,2]}
curl -s -X POST $BASE/api/v0/json/minify --data-binary @file.json
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.
| param | type | description | |
|---|---|---|---|
| pointer | string | one of | a JSON Pointer, e.g. /store/book/0/title; names its own syntax |
| path | string | one of | a JSONPath, e.g. $..book[?(@.price < 10)]; names its own syntax |
| expression | string | one of | either one — the syntax is worked out from it (only a JSONPath starts with $) |
| syntax | string | optional | pointer or jsonpath, when you would rather say than have it guessed |
{
"expression": "$..book[?(@.price < 10)].title",
"syntax": "jsonpath",
"count": 1,
"matches": ["Moby Dick"],
"paths": ["$['store']['book'][0]['title']"]
}
curl -s -X POST "$BASE/api/v0/json/query?pointer=/store/book/0/title" \ --data-binary @file.json
curl -s -X POST "$BASE/api/v0/json/query" \ --data-urlencode 'path=$..book[?(@.price < 10)].title' -G --data-binary @file.json
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.
| field | type | description | |
|---|---|---|---|
| from | any | required | the document as it is |
| to | any | required | the document as it should be |
{
"equal": false,
"operations": 3,
"patch": [
{"op": "remove", "path": "/gone"},
{"op": "replace", "path": "/a", "value": 2},
{"op": "add", "path": "/list/1", "value": 9}
]
}
curl -s -X POST $BASE/api/v0/json/diff \
-H 'Content-Type: application/json' \
-d '{"from":{"a":1,"gone":true},"to":{"a":2}}'
The delete token is returned only once, at creation time. Without it a short url cannot be removed.
Creates a short url pointing at url.
| field | type | description | |
|---|---|---|---|
| url | string | required | target url |
| name | string | optional | human readable label |
| description | string | optional | free form note |
{
"id": "aB3xY",
"token": "8f14e45f-…"
}
url missing or invalid
curl -s -X POST $BASE/api/v0/short-url \
-H 'Content-Type: application/json' \
-d '{"url":"https://example.com"}'
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.
| field | type | description | |
|---|---|---|---|
| url | string | required | target url |
| name | string | optional | human readable label |
| description | string | optional | free form note |
| baseUrl | string | optional | the origin the code should point at; derived from the request host (and X-Forwarded-*) when absent |
| format, size, errorCorrection, foreground, background, margin | optional | as in qr / generate |
{
"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…"
}
url missing or invalid, a bad baseUrl, or a QR option out of range
500rendering failed
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}'
Resolves a short url and redirects to the target. This is the link you share.
| param | type | description | |
|---|---|---|---|
| id | string | required | short id from creation |
id missing
404unknown id
curl -sI $BASE/api/v0/short-url/aB3xY
Deletes a short url. The token may be passed as a query param or in the JSON body.
| param | in | description | |
|---|---|---|---|
| id | path | required | short id |
| token | query / body | required | delete token from creation |
curl -s -X DELETE "$BASE/api/v0/short-url/aB3xY?token=<token>"
A repository is a token-protected bag of links. The token is shown only in the
create response — every other call needs it as ?token=. A wrong token is answered
with 404 on purpose, so repository ids cannot be probed.
Creates a new link repository. Note the trailing slash — it is part of the route.
| field | type | description | |
|---|---|---|---|
| name | string | optional | repository label |
{
"repositoryId": "3f1c…-uuid",
"token": "1a2b…-uuid",
"name": "my links",
"createdAt": "2026-07-22T10:15:30Z"
}
curl -s -X POST $BASE/api/v0/repository/links/ \
-H 'Content-Type: application/json' \
-d '{"name":"my links"}'
Returns the repository with all of its links. The token is never echoed back.
| param | in | description | |
|---|---|---|---|
| repositoryId | path | required | repository uuid |
| token | query | required | repository token |
{
"repositoryId": "3f1c…-uuid",
"name": "my links",
"createdAt": "2026-07-22T10:15:30Z",
"links": [
{
"uuid": "9d0e…-uuid",
"url": "https://example.com",
"name": "example",
"description": ""
}
]
}
curl -s "$BASE/api/v0/repository/links/<id>?token=<token>"
Deletes a repository together with all links inside it.
| param | in | description | |
|---|---|---|---|
| repositoryId | path | required | repository uuid |
| token | query | required | repository token |
curl -s -X DELETE "$BASE/api/v0/repository/links/<id>?token=<token>"
Adds a link to the repository.
| param | in | description | |
|---|---|---|---|
| repositoryId | path | required | repository uuid |
| token | query | required | repository token |
| url | body | required | link target |
| name | body | optional | link label |
| description | body | optional | free form note |
{
"uuid": "9d0e…-uuid",
"url": "https://example.com",
"name": "example",
"description": ""
}
curl -s -X POST "$BASE/api/v0/repository/links/<id>/links?token=<token>" \
-H 'Content-Type: application/json' \
-d '{"url":"https://example.com","name":"example"}'
Replaces the stored link with the values from the body.
| param | in | description | |
|---|---|---|---|
| repositoryId | path | required | repository uuid |
| uuid | path | required | link uuid |
| token | query | required | repository token |
| url | body | required | new link target |
| name | body | optional | new label |
| description | body | optional | new note |
url blank
404repository or link not found
curl -s -X PUT "$BASE/api/v0/repository/links/<id>/links/<uuid>?token=<token>" \
-H 'Content-Type: application/json' \
-d '{"url":"https://example.org"}'
Removes a single link from the repository.
| param | in | description | |
|---|---|---|---|
| repositoryId | path | required | repository uuid |
| uuid | path | required | link uuid |
| token | query | required | repository token |
curl -s -X DELETE "$BASE/api/v0/repository/links/<id>/links/<uuid>?token=<token>"
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.
Uploads a single file as multipart/form-data.
| field | type | description | |
|---|---|---|---|
| file | file | required | the file itself; exactly one |
| name | string | optional | display name, defaults to the original filename |
| ttl | number | optional | lifetime in seconds; 0 or absent means no expiry |
{
"uuid": "7c2d…-uuid",
"token": "4e5f…-uuid",
"name": "report.pdf",
"expiredTime": 1785000000000
}
curl -s -X POST $BASE/api/v0/uploaded-files \ -F '[email protected]' \ -F 'name=report.pdf' \ -F 'ttl=3600'
Returns the configured upload limit, so clients can reject oversized files before sending them.
{
"megabytes": 100,
"bytes": 104857600
}
curl -s $BASE/api/v0/uploaded-files/max-size
Metadata of one file. The token is never included in the response.
| param | in | description | |
|---|---|---|---|
| uuid | path | required | file uuid |
| token | query | required | file access token |
{
"uuid": "7c2d…-uuid",
"name": "report.pdf",
"expiredTime": 1785000000000
}
curl -s "$BASE/api/v0/uploaded-files/<uuid>?token=<token>"
Streams the file content as application/octet-stream with a
Content-Disposition attachment header.
| param | in | description | |
|---|---|---|---|
| uuid | path | required | file uuid |
| token | query | required | file access token |
curl -sOJ "$BASE/api/v0/uploaded-files/<uuid>/download?token=<token>"
Deletes the file and its metadata. The token may be a query param or a JSON body field.
| param | in | description | |
|---|---|---|---|
| uuid | path | required | file uuid |
| token | query / body | required | file access token |
curl -s -X DELETE "$BASE/api/v0/uploaded-files/<uuid>?token=<token>"
Listing every uploaded file is deliberately not allowed — the route exists but always refuses. Files are reachable only by uuid plus token.
{
"error": "Listing all files is not allowed"
}
A link whose text is revealed exactly once and then destroyed. The public
GET /b/{id} only serves the reveal page — the secret is burned by
POST /api/v0/burn/{id}/reveal, so messaging-app link previews (which only GET) cannot
consume it. Missing, expired, already opened and access-blocked links all answer with the exact same
404, so filters cannot be probed.
Creates a burn link. Returns the public id, the ready-to-share url,
and the delete token (shown only here).
| field | type | description | |
|---|---|---|---|
| text | string | required | the secret, max 100000 characters; delivered as data, never rendered as markup |
| name | string | optional | human readable label |
| ttl | number | optional | lifetime in seconds; 0 or absent means no expiry (only the first open destroys it) |
| devices | string[] | optional | allowed devices: iphone, ipad, android, windows, mac, linux, other; empty/absent allows all |
| browsers | string[] | optional | allowed browsers: edge, opera, firefox, chrome, safari, other; empty/absent allows all |
| ipWhitelist | string[] | optional | allowed IPv4/IPv6 addresses or CIDR blocks; empty/absent allows all |
| ipBlacklist | string[] | optional | denied IPv4/IPv6 addresses or CIDR blocks; checked before the whitelist |
| baseUrl | string | optional | origin the url should carry; derived from the request host (and X-Forwarded-*) when absent |
{
"id": "aB3xY9kLmN",
"token": "8f14e45f-…",
"url": "https://ss.serbekun.com/b/aB3xY9kLmN",
"expiredTime": 0,
"devices": ["iphone"],
"browsers": ["firefox"],
"ipWhitelist": ["203.0.113.0/24"],
"ipBlacklist": []
}
text missing or too long, a negative ttl, an unknown device/browser, an invalid IP, or a bad baseUrl
curl -s -X POST $BASE/api/v0/burn \
-H 'Content-Type: application/json' \
-d '{"text":"the secret","ttl":3600,"devices":["iphone"],"browsers":["firefox"],"ipWhitelist":["203.0.113.0/24"]}'
Serves the reveal page with a button. It never reveals or burns anything, so it is safe to
open (and safe for preview crawlers to fetch). A blocked visitor and a missing link both
receive the same 404.
| param | type | description | |
|---|---|---|---|
| id | string | required | public id from creation |
curl -s "$BASE/b/aB3xY9kLmN"
The only call that burns: it returns the secret and permanently removes the record. A caller
whose device, browser or IP is not allowed is answered with the same 404 as a
missing link, and the secret is not burned.
| param | in | description | |
|---|---|---|---|
| id | path | required | public id from creation |
{
"text": "the secret"
}
curl -s -X POST "$BASE/api/v0/burn/aB3xY9kLmN/reveal"
Deletes a burn link early. The token may be passed as a query param or in the JSON body.
| param | in | description | |
|---|---|---|---|
| id | path | required | public id |
| token | query / body | required | delete token from creation |
curl -s -X DELETE "$BASE/api/v0/burn/aB3xY9kLmN?token=<token>"
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.
Returns the raw yt-dlp metadata JSON for a video.
| param | type | description | |
|---|---|---|---|
| url | string | required | video url |
url missing or rejected
500yt-dlp failure
curl -s "$BASE/api/v0/youtube/info?url=https://youtu.be/<id>"
Downloads the video on the server and returns the MP4 bytes (video/mp4).
| param | type | description | |
|---|---|---|---|
| url | string | required | video url |
url missing or rejected
500yt-dlp failure
curl -s -o video.mp4 "$BASE/api/v0/youtube/download?url=https://youtu.be/<id>"
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.
{
"ip": "203.0.113.7"
}
curl -s $BASE/api/v0/network/ip
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.
The index page listing every service.
The site favicon (ss_icon.svg).
Lists the files available in a category.
| category | content |
|---|---|
| html | service pages |
| css | stylesheets |
| js | client scripts |
| images | raster images |
| svg | vector images |
| json | static datasets |
| documents | |
| domain | domain files |
curl -s $BASE/static/v0/json
Returns one static file with the content type of its category.
curl -s $BASE/static/v0/html/api_docs.html