# Fetchext > Fetchext hosts small plain text files and exposes them over a tiny HTTP API. > Apps use the file as a simple database (for example a JSON document of todos), > and the owner can open https://fetchext.arashtaher.com/edit at any time to read or fix the data by hand. > No SDK is needed: it is plain HTTP with a bearer key. This document teaches an AI coding assistant everything needed to use the API. ## Concepts - **File URL**: `https://fetchext.arashtaher.com/f/{id}`, where `{id}` is a random 10-character string. The owner copies it from https://fetchext.arashtaher.com/edit. - **Content**: UTF-8 plain text, served exactly as stored. The size limit depends on the owner's plan: 1024 bytes on Free, 10240 bytes on Pro. Every write response tells you the limit in `max_bytes`. - **Visibility**: a *public* file can be read by anyone with the URL. A *private* file needs a key to read. Writing always needs the read-write key. - **Keys**: each file has two keys, created on https://fetchext.arashtaher.com/edit: - read-only key `txt_ro_...`: can read the file (also when private); - read-write key `txt_rw_...`: can read, replace and append. Send a key as `Authorization: Bearer `. - **ETag**: every successful response carries an `ETag` header (for example `"7"`) that changes whenever the content changes. Use it to avoid overwriting someone else's change (see below). ## Endpoints | Method | Path | Key | Body | Result | |--------|------|-----|------|--------| | GET | `/f/{id}` | none if public, else either key | | the raw text, `text/plain; charset=utf-8` | | PUT | `/f/{id}` | read-write | the new full content | replaces the content | | POST | `/f/{id}/append` | read-write | one line of text | appends it as a new line, atomically | - `GET` supports `If-None-Match: ""` and answers `304 Not Modified` when nothing changed. - `PUT` supports `If-Match: ""` and answers `412 Precondition Failed` when the file changed since. - `PUT` and `append` answer `200` with JSON: `{"etag": "\"8\"", "bytes": 123, "max_bytes": 1024}`. - `append` adds a newline before the line if the file does not end with one, and one after it. Trailing newlines in the body are ignored. - CORS is open to any origin, so browser apps can call the API directly. ## Errors Errors are JSON with a message and a hint on how to fix the request: ```json {"error": "This key is read-only", "hint": "Send the read-write key in the Authorization header as 'Bearer txt_rw_...'. ..."} ``` | Status | Meaning | |--------|---------| | 400 | malformed request (bad `If-Match`, empty append body, content that is not UTF-8 text) | | 401 | missing or invalid key | | 403 | the key is read-only and the request writes, or the file is an extra one beyond the owner's plan (read-only) | | 404 | no file with this id | | 412 | `If-Match` did not match: the file changed since you read it | | 413 | the content would exceed the file's size limit (`max_bytes`) | | 429 | rate limited: wait the number of seconds in `Retry-After` | Read the `hint` field and follow it. ## Examples with curl ```sh export FETCHEXT_URL="https://fetchext.arashtaher.com/f/YOUR_FILE_ID" export FETCHEXT_READ_KEY="txt_ro_..." export FETCHEXT_WRITE_KEY="txt_rw_..." # Read (add -i to see the ETag header) curl -i -H "Authorization: Bearer $FETCHEXT_READ_KEY" "$FETCHEXT_URL" # Replace the whole file, only if it is still at version "7" curl -X PUT "$FETCHEXT_URL" \ -H "Authorization: Bearer $FETCHEXT_WRITE_KEY" \ -H 'If-Match: "7"' \ --data-binary '{"todos": [{"text": "Buy milk", "done": false}]}' # Append one line curl -X POST "$FETCHEXT_URL/append" \ -H "Authorization: Bearer $FETCHEXT_WRITE_KEY" \ --data-binary '{"at": "2026-01-01T10:00:00Z", "event": "signup"}' ``` Use `--data-binary`, not `-d`: `-d` strips newlines. ## Examples with fetch (JavaScript) ```js const URL = process.env.FETCHEXT_URL; const READ_KEY = process.env.FETCHEXT_READ_KEY; const WRITE_KEY = process.env.FETCHEXT_WRITE_KEY; async function read() { const res = await fetch(URL, { headers: { Authorization: `Bearer ${READ_KEY}` } }); if (!res.ok) throw new Error((await res.json()).hint); return { text: await res.text(), etag: res.headers.get("ETag") }; } async function write(text, etag) { const res = await fetch(URL, { method: "PUT", headers: { Authorization: `Bearer ${WRITE_KEY}`, "If-Match": etag }, body: text, }); if (res.status === 412) return null; // changed meanwhile: read again and retry if (!res.ok) throw new Error((await res.json()).hint); return res.headers.get("ETag"); } ``` ## Pattern 1: the whole file is one JSON document Best for small state that changes in place (settings, a todo list, a shopping list). 1. `GET` the file, keep the `ETag`. Parse it with `JSON.parse` (treat an empty file as your initial value). 2. Change the data in memory. 3. `PUT` the new JSON with `If-Match: `. 4. On `412`, someone else wrote first: go back to step 1 and re-apply your change. Retry a few times. ```js async function update(change) { for (let attempt = 0; attempt < 5; attempt++) { const { text, etag } = await read(); const data = text.trim() ? JSON.parse(text) : { todos: [] }; change(data); if (await write(JSON.stringify(data, null, 2), etag)) return data; } throw new Error("Too many concurrent updates"); } await update((data) => data.todos.push({ text: "Buy milk", done: false })); ``` Pretty-print the JSON (`JSON.stringify(data, null, 2)`) so the owner can edit it by hand. Owners may also edit the file in the browser, so parse defensively and handle invalid JSON gracefully. ## Pattern 2: one record per line, with append Best for logs, events, guestbooks, form submissions: data that is only added. - Write: `POST /f/{id}/append` with one record, e.g. a JSON object on a single line (JSON Lines). Appends are atomic, so concurrent writers never lose lines and no `If-Match` is needed. - Read: `GET` the file, split on `\n`, skip empty lines, parse each line. - Clean up: when `bytes` gets close to `max_bytes`, `PUT` a trimmed version (with `If-Match`). ```js await fetch(`${URL}/append`, { method: "POST", headers: { Authorization: `Bearer ${WRITE_KEY}` }, body: JSON.stringify({ at: new Date().toISOString(), name: "Ada" }), }); const records = (await read()).text.split("\n").filter(Boolean).map((line) => JSON.parse(line)); ``` ## Avoiding lost writes with ETag and If-Match Two clients that read, change and `PUT` at the same time would otherwise overwrite each other. Always send the `ETag` from your last read as `If-Match` on `PUT`. The server only writes if the file is still at that version, otherwise it answers `412` with the current `ETag`. Never retry a `412` blindly with the new ETag: read the file again and re-apply your change to the new content. Without `If-Match`, a `PUT` overwrites unconditionally. `append` needs no `If-Match`. To poll cheaply, send `If-None-Match` with the last ETag; a `304` means nothing changed. ## Limits - File size: 1024 bytes of UTF-8 text on Free, 10240 on Pro; read `max_bytes` from any write response. Larger writes get `413` and change nothing. Design for the small limit: store compact data and prune old entries. - Rate limits: 60 requests per minute per key and 120 per IP address. Over the limit you get `429` with a `Retry-After` header (seconds). - 2 files per account on Free, 10 on Pro; each has its own URL and keys. ## Security: keep the read-write key out of frontend code Anything shipped to the browser, including environment variables bundled into frontend code (`NEXT_PUBLIC_*`, `VITE_*`, ...), is **visible to everyone** who opens the app. Whoever has the read-write key can replace or wipe the file. - Keep keys in environment variables, never commit them. - Call the API with the read-write key from a server, an API route or a serverless function. - In frontend-only apps, only use a read-only key or a public file for reading, or accept that anyone can change the data (fine for a personal prototype, not for real users). - If a key leaks, the owner regenerates it on https://fetchext.arashtaher.com/edit; the old key stops working at once.