Example app: The action — create & read notes
The two endpoints an agent actually uses — GET and POST /api/notes — with query filters, zod body validation, response shapes, and a worked end-to-end example
Example app: The action — create & read notes
This page documents the two endpoints an agent uses constantly — reading notes and creating notes — against the real handlers in apps/web/src/app/api/notes/route.ts. By the end you can replay the entire flow by hand.
Base URL and auth
All requests go to https://notes.aawej.in and carry the session token in the x-session-token header (see REST API integration for how that header is verified). The user must already have an account at notes.aawej.in — there is no auto-provisioning, and the email in the token is the join key to the local account.
GET /api/notes — read notes
Lists the authenticated user's notes, newest-updated first.
GET /api/notes?search=meeting&limit=10&offset=0
x-session-token: <session-token>Query parameters
| Param | Type | Description |
|---|---|---|
search | string | Free-text search — matches note titles and the plain-text body |
tagId | UUID | Only notes with this tag |
limit | integer | Max notes to return, 1–500 (default 100) |
offset | integer | Pagination offset (default 0) |
All parameters are optional. Invalid values — a non-UUID tagId, a limit of 0 or 501 — return:
{ "error": "Invalid query parameters", "details": { ... } }with status 400.
Response — 200 OK
{
"notes": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"title": "Meeting notes",
"content": [],
"contentText": "Discussed the Q3 roadmap and the partner integration.",
"createdAt": "2026-07-12T05:34:00.889Z",
"updatedAt": "2026-07-12T05:34:14.142Z",
"tags": [
{
"id": "660e8400-e29b-41d4-a716-446655440001",
"name": "work",
"createdAt": "2026-07-10T12:00:00.000Z"
}
]
}
],
"total": 1
}notes— the page of notes. Each note has anid(UUID),title,content(BlockNote/ProseMirror JSON, possibly empty),contentText(plain text for search), ISO-8601createdAt/updatedAt, and atagsarray.total— the number of notes matching the filters across all pages, so an agent can paginate withoffset.
POST /api/notes — create a note
Creates a note for the authenticated user.
POST /api/notes
x-session-token: <session-token>
Content-Type: application/json
{ "title": "Grocery List", "tagIds": ["660e8400-e29b-41d4-a716-446655440001"] }Body
The body is validated with zod against this schema:
| Field | Type | Required | Rules |
|---|---|---|---|
title | string | ✅ | trimmed, 1–500 characters |
content | any | — | BlockNote / ProseMirror JSON document (defaults to []) |
contentText | string | — | plain-text representation, used for search |
tagIds | UUID[] | — | tag UUIDs to assign (tags not owned by the user are ignored) |
Errors
| Status | When | Response body |
|---|---|---|
400 | Body is not valid JSON | { "error": "Request body must be valid JSON" } |
400 | Body fails the schema (e.g. missing/empty title) | { "error": "Invalid request body", "details": { ... } } |
Response — 201 Created
{
"note": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"title": "Grocery List",
"content": [],
"contentText": "",
"createdAt": "2026-07-12T05:34:00.889Z",
"updatedAt": "2026-07-12T05:34:00.889Z",
"tags": []
}
}Success is 201, not 200 — a new resource was created. The note is returned in the same serialized shape as GET /api/notes (dates as ISO-8601 strings).
The whole flow, end to end
Putting it together — the exact sequence an agent runs:
Get a session token
aon token getPrints a token valid for 5 minutes. Save it to a variable:
TOKEN=$(aon token get)Read notes with a search
curl -H "x-session-token: $TOKEN" \
"https://notes.aawej.in/api/notes?search=meeting&limit=5"Create a note
curl -X POST -H "x-session-token: $TOKEN" \
-H "Content-Type: application/json" \
-d '{"title":"Grocery List","contentText":"Milk, eggs, bread"}' \
"https://notes.aawej.in/api/notes"Returns 201 with the new note — note its id.
Read it back
curl -H "x-session-token: $TOKEN" \
"https://notes.aawej.in/api/notes?search=Grocery"The new note appears in the results, scoped to the user whose email the token verified to.
Refresh when the token expires
5 minutes later the same calls fail with 401. Run aon token get again — the CLI mints a fresh token from your saved key.
One email, one account, all their notes
Every call is scoped to the user behind the token: search, tagId, create, pagination — all of it happens inside that user's data. That is the payoff of the whole email-is-the-join-key design: the agent never names a user, the token does.
The full endpoint surface
For reference, the complete REST API exposed by the notes app:
| Method | Path | Purpose |
|---|---|---|
GET | /api | API index — lists every endpoint |
GET | /api/notes | List notes (search, tagId, limit, offset) |
GET | /api/notes/:id | Fetch one note by UUID |
POST | /api/notes | Create a note |
PATCH | /api/notes/:id | Update a note (partial body) |
DELETE | /api/notes/:id | Delete a note |
GET | /api/tags | List the user's tags |