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

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.

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:

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.

List

curl -H "$KEY" https://service.lyabah.com/files/img/
{
  "folder": "img",
  "files": [ { "path": "img/chart.png", "size": 20480, "public": true, "…": "…" } ],
  "folders": [ "img/icons/" ]
}

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