Great for vibe-coded appsText hosting
Text hosting
with an API.
Write in the browser, get a URL, and read or update it from any app, script or AI agent. The easy place for a vibe-coded app to keep its settings, lists and data.
- Free
- Two pages, up to 1,000 characters each
- $0
- Pro
- Ten pages, up to 10,000 characters each
- $10 a year
$ curl https://fetchext.arashtaher.com/f/k7qm2xt9bw
Groceries
- oat milk
- coffee beans
# 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 <key>`.
- **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: "<etag>"` and answers `304 Not Modified` when nothing changed.
- `PUT` supports `If-Match: "<etag>"` 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: <etag>`.
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.