Agents: the same text as plain Markdown is at /help.md. Usage numbers: /stats.
service.lyabah.com
An internal service for storing files and text, and for passing data between
people, scripts and AI agents. Base URL: https://service.lyabah.com
/files/…stores any file (images, PDFs, archives, binaries) in object storage. Downloading answers with a redirect to a temporary storage link./wiki/…stores text (JSON, YAML, XML, HTML, Markdown, CSV, plain text) and serves it back as-is with the right content type. Every change is kept in the page's history./statsshows usage./helpis this page;/help.md(also/llms.txt) is the same text as plain Markdown, for agents.
Authentication
Every write, every listing and every read of a private item needs the service key, sent as a header:
Authorization: Bearer $LYABAH_KEY
Items are private by default. Add public=true to an upload (or PATCH
it later) to let anyone with the URL read it — for example an image embedded
in a GitHub README. The key is accepted only in that header: never put it in
a URL.
Quick start
KEY="Authorization: Bearer $LYABAH_KEY"
# Upload a private file, and a public image.
curl -H "$KEY" -T report.pdf https://service.lyabah.com/files/docs/report.pdf
curl -H "$KEY" -T chart.png "https://service.lyabah.com/files/img/chart.png?public=true"
# Download: follow the redirect.
curl -L -H "$KEY" -o report.pdf https://service.lyabah.com/files/docs/report.pdf
# Write a wiki page, read it back, see its history.
curl -H "$KEY" -T config.json https://service.lyabah.com/wiki/project/config.json
curl -H "$KEY" https://service.lyabah.com/wiki/project/config.json
curl -H "$KEY" "https://service.lyabah.com/wiki/project/config.json?history"
Paths
/files/<path> and /wiki/<path>, where <path> is folder/…/name.
- Each segment uses letters, digits,
.,_and-only — no spaces, no%-escapes. Paths are case-sensitive and at most 1024 characters. - Folders are implicit, as in S3: a folder exists while something is in it,
and folders nest (
a/b/c/file.txt). A file may also sit at the root. - A path that ends in
/is the folder itself, which is how you list it./files/and/wiki/list the root.
Files
| Request | What it does |
|---|---|
PUT /files/<path> |
Upload the request body: any bytes, up to 100 MB. Creates or replaces. |
GET /files/<path> |
302 redirect to a temporary download link, valid for 1 hour. HEAD works too. |
GET /files/<path>?meta |
Metadata, as JSON. |
GET /files/<folder>/ |
List a folder. |
PATCH /files/<path> |
Change visibility or expiry; move or rename. |
DELETE /files/<path> |
Delete (204). |
Upload
curl -H "$KEY" -T build.tar.gz "https://service.lyabah.com/files/builds/app-1.4.tar.gz?expires_in=7d"
Query parameters, all optional:
public=trueorpublic=false— who may read it. A new file is private.expires_in=30m|12h|7d|2worexpires_at=2026-12-31T23:59:00Z— delete it automatically after that.expires_in=neverremoves an expiry.
Replacing a file keeps its visibility and expiry unless the upload sets them.
Send If-None-Match: * to refuse to replace an existing file (412).
The stored content type comes from your Content-Type header when you send a
specific one. Otherwise it comes from the file extension (.png, .pdf,
.zip, .tar.gz, .json, …), and otherwise from sniffing the bytes.
curl -T sends no content type, which is fine. Bytes are stored exactly as sent, so
archives come back byte-for-byte.
The answer is 201 for a new file, 200 for a replaced one, with the
metadata:
{
"path": "builds/app-1.4.tar.gz",
"folder": "builds",
"name": "app-1.4.tar.gz",
"url": "https://service.lyabah.com/files/builds/app-1.4.tar.gz",
"size": 1048576,
"content_type": "application/gzip",
"sha256": "9f86d0…",
"public": false,
"expires_at": "2026-10-05T12:00:00Z",
"downloads": 0,
"created_at": "2026-09-28T12:00:00Z",
"updated_at": "2026-09-28T12:00:00Z"
}
Uploads pass through a proxy that ends any request after 60 seconds, so a
large file over a slow link can fail; 100 MB is the hard limit (413).
Download
GET answers 302 with a Location header pointing at a signed storage
URL that works for 1 hour. Follow it (curl -L). Public files need no key.
- Images, PDFs, audio, video and text open in the browser. Archives and other binaries download under their own name.
- Your client must not send
Authorizationto the storage host. curl, Pythonrequestsand Go drop it on a redirect to another host by themselves; with curl, never use--location-trusted. - To get a temporary link to hand to something else, without downloading:
curl -s -o /dev/null -w '%{redirect_url}' -H "$KEY" https://service.lyabah.com/files/docs/report.pdf - Always share the
https://service.lyabah.com/files/…URL itself, not the storage link: the storage link expires.
List
curl -H "$KEY" https://service.lyabah.com/files/img/
{
"folder": "img",
"files": [ { "path": "img/chart.png", "size": 20480, "public": true, "…": "…" } ],
"folders": [ "img/icons/" ]
}
recursive=truelists everything below the folder, with nofolderspart.limit=N(default 1000, at most 10000). When there is more, the answer has"next": "<path>"; pass it back asafter=<path>for the next page.
Change, move, delete
PATCH takes a JSON body. Give any of the fields; the rest stay as they are.
# Make it public.
curl -X PATCH -H "$KEY" -d '{"public": true}' https://service.lyabah.com/files/img/chart.png
# Expire in 3 days; "expires_at": null removes the expiry.
curl -X PATCH -H "$KEY" -d '{"expires_in": "3d"}' https://service.lyabah.com/files/img/chart.png
# Move to another folder, keeping the name (note the trailing /)…
curl -X PATCH -H "$KEY" -d '{"move_to": "archive/2026/"}' https://service.lyabah.com/files/img/chart.png
# …or move and rename.
curl -X PATCH -H "$KEY" -d '{"move_to": "archive/chart-old.png"}' https://service.lyabah.com/files/img/chart.png
curl -X DELETE -H "$KEY" https://service.lyabah.com/files/archive/chart-old.png
Moving onto an existing item answers 409 unless the body also has
"overwrite": true. "move_to": "/" moves to the root.
Wiki
| Request | What it does |
|---|---|
PUT /wiki/<path> |
Create or replace the page with the request body: UTF-8 text, up to 5 MB. |
GET /wiki/<path> |
The page's content, as-is, with its content type. HEAD works too. |
GET /wiki/<path>?history |
Revisions, newest first, as JSON. |
GET /wiki/<path>?rev=N |
The content of revision N. |
GET /wiki/<path>?diff=N |
Unified diff from revision N-1 to N. ?diff=A..B compares any two; 0 is the empty page before revision 1. |
GET /wiki/<path>?meta |
Metadata, as JSON (always needs the key). |
GET /wiki/<folder>/ |
List a folder (the list has pages instead of files). |
PATCH /wiki/<path> |
Same body as for files. Moving keeps the history. |
DELETE /wiki/<path> |
Delete the page and its history (204). |
PUT takes the same public, expires_in and expires_at parameters as a
file upload. It answers 201 for a new page, 200 otherwise, with the
metadata plus "revision" and "changed".
Content type
The page's type comes from your Content-Type header when you send one.
Otherwise it comes from the extension: .json, .yaml/.yml, .xml, .html, .md,
.txt, .csv, .toml, .jsonl, .js, .css, .svg. Anything else is
text/plain. Pages are served with ; charset=utf-8. A body that is not
UTF-8 text is refused with 415; store binaries under /files.
Revisions and safe concurrent writes
Every write that changes the text makes a new revision, numbered from 1.
Writing the same text again makes none ("changed": false). Every answer
carries ETag: "N" and X-Revision: N.
Several agents writing one page should use a read–modify–write loop, so that nobody overwrites a change they have not seen:
curl -s -D head.txt -o todo.md -H "$KEY" https://service.lyabah.com/wiki/team/todo.md # head.txt has ETag: "7"
# …edit todo.md…
curl -H "$KEY" -H 'If-Match: "7"' -T todo.md https://service.lyabah.com/wiki/team/todo.md
A 412 means someone else wrote first: read the page again, reapply your
change and retry. If-None-Match: * on a PUT only creates, never replaces.
To poll cheaply, send If-None-Match: "7" on a GET: the answer is 304
until the page changes.
Optional headers X-Author: <who> and X-Message: <why> on a PUT are
stored with the revision and shown in ?history:
{
"path": "team/todo.md",
"revision": 8,
"revisions": [
{ "revision": 8, "created_at": "2026-09-28T12:00:00Z", "author": "planner-agent",
"message": "add deploy step", "content_type": "text/markdown", "size": 812,
"sha256": "…", "lines_added": 1, "lines_removed": 0 }
]
}
Expiry
Files and wiki pages can expire. Set expires_in (30m, 12h, 7d, 2w)
or expires_at (RFC 3339) as a query parameter on a PUT, or as a JSON field
on a PATCH, where "expires_at": null removes it. An expired item disappears
at once and is deleted for good within about a minute, with a wiki page's
history.
Errors
Errors are JSON: {"error": "what went wrong"}.
| Status | Meaning |
|---|---|
400 |
Bad path, parameter or JSON body. |
401 |
The key is missing or wrong. |
404 |
No such item, or it has expired. |
409 |
A move target exists (send "overwrite": true), or two writes raced (retry). |
412 |
If-Match / If-None-Match did not hold. |
413 |
Body too large: 100 MB for files, 5 MB for wiki pages. |
415 |
Binary body sent to the wiki. |
502 |
Object storage failed. GET /health/storage (with the key) walks each storage operation and says which one fails and why. |
Tips for agents
- Pass messages through a wiki page: a JSON or JSONL page per channel,
written with
If-Matchso that concurrent writers never lose an update, and read withIf-None-Matchto poll. Addexpires_into anything temporary. - Show an image on GitHub: upload it with
public=trueand use thehttps://service.lyabah.com/files/…URL in the Markdown. GitHub caches images, so upload a changed image under a new name to show the new one right away. - Check before you overwrite:
GET …?metareturns thesha256of a file, and a wiki page'sETagis its revision number.