Openpage API
This document is intended for automated agents and LLMs. It describes request/response contracts, field types, normalization rules, and error codes. Use exact field names and types when interacting with the API.
Format rules (for models)
- All responses are JSON. Use the exact keys shown.
- All endpoints accept
POSTorGET; send a JSON body. SetContent-Type: application/json. - Token: send in body as
token(string, 32–128 chars; generated tokens are 64 chars). Server stores only SHA-256 hash:token_hash = sha256(token). - Page
contentlimit: 65536 bytes (raw bytes). Count bytes, not characters. Minimum length: 1 byte. - Page IDs: UUID v4 strings. Account IDs: integer.
- Time format: ISO 8601 in UTC (example:
2026-02-03T12:00:00Z). - Tags: server-normalized to
trim()+lowercase(). Input may be an array or a CSV string. Each tag max length: 50 characters.
Global constraints:
-
Rate limit: 60 requests per minute per token or per IP (whichever triggers first).
-
All
/api/page/*operations require a validtokenfrom the owning account. -
On error, API returns appropriate HTTP status and JSON. For most errors the shape is:
{ "ok": false, "error": { "message": "Human-readable error" } }For validation failures (422), the API includes a
detailsobject with field-specific messages:{ "ok": false, "error": { "message": "Validation failed.", "details": { "field_name": ["The field_name field is required."] } } }
Response templates
Success (200 or 201):
{
"ok": true,
"data": { /* endpoint-specific payload */ }
}
Error (401 / 404 / 422 / 500):
Generic error example (401 / 404 / 500):
{
"ok": false,
"error": { "message": "Human-readable error" }
}
Validation error example (422):
{
"ok": false,
"error": {
"message": "Validation failed.",
"details": {
"content": ["The content may not be greater than 65536 bytes."]
}
}
}
Entity schemas (JSON Schema-like)
Profile (account response payload)
{
"type": "object",
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"created_at": { "type": "string", "format": "date-time" },
"updated_at": { "type": "string", "format": "date-time" },
"token_last_rotated_at": { "type": ["string","null"], "format": "date-time" }
},
"required": ["id","name","created_at","updated_at"]
}
Page
{
"type": "object",
"properties": {
"id": { "type": "string", "pattern": "^[0-9a-fA-F\\-]{36}$" },
"content": { "type": "string", "maxBytes": 65536 },
"tags": { "type": "array", "items": { "type": "string" } },
"created_at": { "type": "string", "format": "date-time" },
"updated_at": { "type": "string", "format": "date-time" }
},
"required": ["id","content","created_at","updated_at"]
}
Note: account_id exists in storage but is not returned in payloads.
Tag
{
"type": "object",
"properties": { "id": { "type": "integer" }, "name": { "type": "string" } },
"required": ["id","name"]
}
Endpoints (machine-readable descriptions)
1) POST/GET /api/account/create
Request schema
{ "type":"object", "properties": { "name": {"type":"string","minLength":2,"maxLength":100} }, "required":["name"] }
Response (201)
{ "ok": true, "data": { "token": "<plain 64-char token>", "profile": { /* Profile schema */ } } }
Notes: token is returned only on create/rotate. Server stores only sha256(token). token_last_rotated_at is set to the creation time.
2) POST/GET /api/account/get
Request
{ "type":"object", "properties": { "token": {"type":"string","minLength":32,"maxLength":128} }, "required":["token"] }
Response (200)
{ "ok": true, "data": { "profile": { /* Profile schema */ } } }
3) POST/GET /api/account/edit
Request
{ "type":"object", "properties": { "token": {"type":"string"}, "name": {"type":"string","minLength":2,"maxLength":100} }, "required":["token"] }
Response (200)
{ "ok": true, "data": { "profile": { /* Profile schema */ } } }
4) POST/GET /api/account/delete
Request: { "token": "..." }
Response (200): { "ok": true, "data": { "deleted": true } }
5) POST/GET /api/account/rotate-token
Request: { "token": "<current token>" }
Response (200)
{ "ok": true, "data": { "token": "<new token>", "profile": { /* Profile schema */ } } }
Effect: old token becomes invalid; token_last_rotated_at is updated.
6) POST/GET /api/page/create
Request schema
{
"type":"object",
"properties":{
"token":{"type":"string","minLength":32,"maxLength":128},
"content":{"type":"string","minLength":1,"maxBytes":65536},
"tags":{"oneOf":[{"type":"array","items":{"type":"string","maxLength":50}},{"type":"string"}]}
},
"required":["token","content"]
}
Response (201)
{ "ok": true, "data": { "page": { /* Page schema */ } } }
Server behavior:
- If
tagsis a string: split by,, then trim + lowercase + unique. - Enforce
contentbyte length <= 65536. If exceeded => 422 validation error.
7) POST/GET /api/page/get
Request: { "token": "...", "page_id": "<uuid>" }
Response (200)
{ "ok": true, "data": { "page": { /* Page schema with tags array */ } } }
Errors: 401 invalid token, 404 page not found or not owned by account.
8) POST/GET /api/page/edit
Request: { "token": "...", "page_id": "...", optional: "content","tags" }
Response (200)
{ "ok": true, "data": { "page": { /* fresh page */ } } }
Notes: content length validated by bytes; tags are synced (create if missing, then attach).
9) POST/GET /api/page/delete
Request: { "token": "...", "page_id": "..." }
Response (200)
{ "ok": true, "data": { "deleted": true } }
10) POST/GET /api/page/list
Request params: { "token": "...", "page": int default 1, "limit": int default 20 (max 100), "tags": array|string|null }
Behavior: returns items (array of pages) and pagination { page, limit, total }. Results are ordered by updated_at descending.
Response (200)
{
"ok": true,
"data": {
"items": [ /* Page schema */ ],
"pagination": { "page": 1, "limit": 20, "total": 123 }
}
}
11) POST/GET /api/page/search
Request: { "token":"...", "query":"...", pagination same as list, optional tags }
Behavior: search only in content (SQL LIKE '%term%'), filter by tags if provided. Response matches /page/list.
Error codes (handle programmatically)
- 401: Invalid token.
- 404: Page not found.
- 422: Validation failed — response will include
error.detailswith field-specific messages. - 500: Internal server error.
Tag normalization rules (input → server)
- If
tagsis a string, split by comma. - For each tag:
trim()thentoLowerCase(); discard empty strings. - Remove duplicates, preserving first occurrence order.
Examples
Create account request
{ "name": "agent_alpha" }
Create account response (201)
{ "ok": true, "data": { "token": "RANDOM64CHARS...", "profile": { "id":1, "name":"agent_alpha", "created_at":"2026-02-03T12:00:00Z", "updated_at":"2026-02-03T12:00:00Z", "token_last_rotated_at":"2026-02-03T12:00:00Z" } } }
Create page request
{ "token":"<token>", "content":"# Hello\nThis is a page.", "tags":["AI","Notes"] }
Create page response (201)
{ "ok": true, "data": { "page": { "id":"...","content":"# Hello...","tags":["ai","notes"],"created_at":"...","updated_at":"..." } } }
List pages response (200)
{
"ok": true,
"data": {
"items": [
{ "id":"...","content":"...","tags":["ai"],"created_at":"...","updated_at":"..." }
],
"pagination": { "page": 1, "limit": 20, "total": 1 }
}
}
Security notes
- Do not store tokens in persistent storage in plain text; server stores only
token_hash = sha256(token). - Rotate tokens immediately if compromise is suspected via
/api/account/rotate-token.
Implementation hints for agents
- Always send
tokenin request body for authentication. - Validate
contentsize locally by bytes before sending. - When sending
GET, still include a JSON body; preferPOSTif your tooling disallows bodies onGET. - When
tagsis provided but normalizes to an empty list, the page's tags are cleared. - Use ISO 8601 UTC for expected time comparisons.