Posting notes¶
Send the note's markdown to POST /api/notes with the token as a bearer token:
curl -H "Authorization: Bearer $NOTEFEED_TOKEN" \
--data-binary @note.md \
https://notes.example.com/api/notes
notefeed stores the body exactly as sent, byte for byte, and answers 201 Created:
{
"id": "20260929T140512Z-backup-finished",
"url": "https://notes.example.com/n/20260929T140512Z-backup-finished"
}
Use --data-binary, not -d
curl -d strips newlines from files. --data-binary sends the file unchanged.
Formats¶
| Content type | Treated as |
|---|---|
| none | raw markdown |
text/markdown, text/plain |
raw markdown |
application/x-www-form-urlencoded |
raw markdown (what curl --data-binary sends by default) |
application/json |
an object with a string field markdown |
Any other content type is rejected with 415. The body must be UTF-8.
From Python or Node, use the client libraries instead of building requests yourself. From other languages, JSON is often easier than a raw body:
curl -H "Authorization: Bearer $NOTEFEED_TOKEN" \
-H "Content-Type: application/json" \
--data-binary @- https://notes.example.com/api/notes <<'EOF'
{
"markdown": "# Deploy done\nversion 1.4.2 on host-2"
}
EOF
Titles and filenames¶
- Title: the first
#heading. Without one, it's the first non-empty line, with list and quote markers removed.#lines inside fenced code blocks are ignored. Titles are cut to 100 characters. - File:
<DATA_DIR>/<id>.md, where the id is the UTC time to the second plus a slug of the title, e.g.20260929T140512Z-backup-finished. Accented letters become plain ones (Café→cafe). A title with no usable letters becomesnote. - Collisions: two notes with the same title in the same second get
-2,-3and so on. An existing note is never overwritten.
Errors¶
| Status | When |
|---|---|
400 |
The note is empty; the JSON is invalid or has no string markdown; the body is not UTF-8 |
401 |
The Authorization header is missing or the token is wrong |
413 |
The body is larger than 100 KB (102400 bytes) |
415 |
The content type is not one of those above |
500 |
The note could not be written. No partial file is left behind. |
Error responses are JSON: {"error": "<short reason>"}.
From a script¶
The client libraries handle the request, the token and the errors for you, and bring a notefeed command for shell scripts. Plain curl works everywhere else.
A backup job that reports how it went:
#!/usr/bin/env bash
set -euo pipefail
# NOTEFEED_URL and NOTEFEED_TOKEN come from the environment.
if output=$(restic backup /srv 2>&1); then status="finished"; else status="FAILED"; fi
printf '# Backup %s on %s\n\n```\n%s\n```\n' "$status" "$(hostname)" "$(tail -n 5 <<<"$output")" |
notefeed post - > /dev/null
#!/usr/bin/env bash
set -euo pipefail
if output=$(restic backup /srv 2>&1); then status="finished"; else status="FAILED"; fi
printf '# Backup %s on %s\n\n```\n%s\n```\n' "$status" "$(hostname)" "$(tail -n 5 <<<"$output")" |
curl -fsS -H "Authorization: Bearer $NOTEFEED_TOKEN" --data-binary @- \
https://notes.example.com/api/notes > /dev/null
From a program, with the client libraries:
import os, shutil
from notefeed import Client
client = Client("https://notes.example.com", os.environ["NOTEFEED_TOKEN"])
usage = shutil.disk_usage("/srv")
used = usage.used / usage.total
if used > 0.9:
print(client.post(f"# Disk space low\n/srv is {used:.0%} full").url)
import { statfs } from "node:fs/promises";
import { Client } from "notefeed";
const client = new Client({ url: "https://notes.example.com", token: process.env.NOTEFEED_TOKEN });
const fs = await statfs("/srv");
const used = 1 - fs.bavail / fs.blocks;
if (used > 0.9) console.log((await client.post(`# Disk space low\n/srv is ${Math.round(used * 100)}% full`)).url);