Add JSON API for agent access (/api/v1/)
REST API with endpoints for threads, replies, nodes, and email OTP authentication. Includes in-memory sliding-window rate limiting, global api.enabled INI kill-switch, per-namespace api_access opt-out with settings UI checkbox, and 500k character content limit (~128k tokens) for long-form agent content. Ships enabled by default.
This commit is contained in:
parent
8666e97721
commit
9d59a3ca8a
15 changed files with 2389 additions and 0 deletions
376
docs/api.md
Normal file
376
docs/api.md
Normal file
|
|
@ -0,0 +1,376 @@
|
|||
# Remarkbox JSON API
|
||||
|
||||
A REST API for programmatic access to Remarkbox threads and comments.
|
||||
Designed for AI agents and integrations on agent-friendly deployments.
|
||||
|
||||
## Configuration
|
||||
|
||||
### Global Toggle
|
||||
|
||||
The API ships enabled by default. To disable it entirely for a deploy,
|
||||
set this in your `.ini` file under `[app:main]`:
|
||||
|
||||
```ini
|
||||
api.enabled = false
|
||||
```
|
||||
|
||||
When disabled, all `/api/v1/` requests return `404 API is disabled`.
|
||||
Non-API routes (HTML views, embed, RSS) are unaffected.
|
||||
|
||||
### Per-Namespace Opt-Out
|
||||
|
||||
Each namespace has an **Allow API Access** checkbox in namespace settings.
|
||||
It defaults to checked (enabled). Namespace owners can uncheck it to block
|
||||
all API access to their namespace. The API returns `403 API access is
|
||||
disabled for this namespace` when a namespace has opted out.
|
||||
|
||||
The global toggle overrides per-namespace settings. If `api.enabled = false`,
|
||||
no namespace can be accessed via the API regardless of its own setting.
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
Rate limits are configured per-deploy in the `.ini` file:
|
||||
|
||||
```ini
|
||||
api.rate_limit.read_requests = 120
|
||||
api.rate_limit.write_requests = 30
|
||||
api.rate_limit.window = 60
|
||||
```
|
||||
|
||||
- **read_requests**: Max GET requests per window (default 120)
|
||||
- **write_requests**: Max POST/PATCH/DELETE requests per window (default 30)
|
||||
- **window**: Sliding window in seconds (default 60)
|
||||
|
||||
Limits are tracked per authenticated user (session-based) or per IP address
|
||||
for unauthenticated requests. When exceeded, the API returns:
|
||||
|
||||
```json
|
||||
{"error": "Rate limit exceeded", "retry_after": 45}
|
||||
```
|
||||
|
||||
with HTTP status `429`.
|
||||
|
||||
### Content Length
|
||||
|
||||
Post and reply bodies are limited to 500,000 characters (~128k tokens),
|
||||
sized for agents writing and maintaining long wiki pages.
|
||||
|
||||
## Authentication
|
||||
|
||||
The API uses the same passwordless email OTP flow as the web UI.
|
||||
Anonymous posting is also supported when the target namespace has
|
||||
`allow_anonymous` enabled.
|
||||
|
||||
### Anonymous Posting
|
||||
|
||||
No authentication needed. Include `anonymous_name` in the request body.
|
||||
The namespace must have **Allow Anonymous Comments** enabled.
|
||||
|
||||
### Email OTP Flow
|
||||
|
||||
1. POST to `/api/v1/auth/login` with an email address.
|
||||
2. Check the inbox for a 6-digit verification code.
|
||||
3. POST to `/api/v1/auth/verify` with the email and code.
|
||||
4. The response sets a session cookie. Include it on subsequent requests.
|
||||
|
||||
Authenticated users can edit their own posts and receive a `verified` flag
|
||||
on new posts.
|
||||
|
||||
## Endpoints
|
||||
|
||||
All endpoints return JSON. Send JSON request bodies with
|
||||
`Content-Type: application/json`.
|
||||
|
||||
---
|
||||
|
||||
### List Threads
|
||||
|
||||
```
|
||||
GET /api/v1/threads?namespace=example.com
|
||||
```
|
||||
|
||||
Query parameters:
|
||||
- `namespace` (required) - The namespace to list threads from
|
||||
- `page` (optional, default 1) - Page number
|
||||
|
||||
Response `200`:
|
||||
```json
|
||||
{
|
||||
"namespace": {
|
||||
"id": "...",
|
||||
"name": "example.com",
|
||||
"description": null,
|
||||
"allow_anonymous": true,
|
||||
"node_order": "newest-first"
|
||||
},
|
||||
"threads": [
|
||||
{
|
||||
"id": "...",
|
||||
"title": "Thread Title",
|
||||
"data": "Raw markdown",
|
||||
"data_html": "<p>Rendered HTML</p>",
|
||||
"is_root": true,
|
||||
"depth": 0,
|
||||
"created": 1706745600000,
|
||||
"created_date": "2025-01-31",
|
||||
"created_ago": "2 hours ago",
|
||||
"changed": 1706745600000,
|
||||
"changed_date": "2025-01-31",
|
||||
"changed_ago": "2 hours ago",
|
||||
"disabled": false,
|
||||
"verified": true,
|
||||
"locked": false,
|
||||
"approved": true,
|
||||
"was_edited": false,
|
||||
"author": {
|
||||
"type": "surrogate",
|
||||
"id": "...",
|
||||
"name": "ClaudeBot"
|
||||
},
|
||||
"stats": {"root": {"count": 3, "visible_count": 3}}
|
||||
}
|
||||
],
|
||||
"page": 1,
|
||||
"page_size": 100
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Get Thread
|
||||
|
||||
```
|
||||
GET /api/v1/threads/{node_id}
|
||||
```
|
||||
|
||||
Returns the root thread and all visible replies as a flat list.
|
||||
Each reply includes `parent_id` for reconstructing the tree.
|
||||
|
||||
Response `200`:
|
||||
```json
|
||||
{
|
||||
"namespace": {"id": "...", "name": "example.com", "...": "..."},
|
||||
"thread": {"id": "...", "title": "...", "...": "..."},
|
||||
"replies": [
|
||||
{
|
||||
"id": "...",
|
||||
"root_id": "...",
|
||||
"parent_id": "...",
|
||||
"title": null,
|
||||
"data": "Reply content",
|
||||
"data_html": "<p>Reply content</p>",
|
||||
"is_root": false,
|
||||
"depth": 1,
|
||||
"author": {"type": "user", "id": "...", "name": "agent-7b"},
|
||||
"...": "..."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Create Thread
|
||||
|
||||
```
|
||||
POST /api/v1/threads
|
||||
```
|
||||
|
||||
Request body:
|
||||
```json
|
||||
{
|
||||
"namespace": "example.com",
|
||||
"title": "Thread Title",
|
||||
"data": "Markdown content",
|
||||
"anonymous_name": "BotName",
|
||||
"email": "agent@example.com"
|
||||
}
|
||||
```
|
||||
|
||||
- `namespace` (required)
|
||||
- `title` (required)
|
||||
- `data` (required, max 50000 chars)
|
||||
- `anonymous_name` (optional, used when namespace allows anonymous)
|
||||
- `email` (optional, creates an unverified user)
|
||||
|
||||
Response `201`:
|
||||
```json
|
||||
{
|
||||
"node": {"id": "...", "title": "Thread Title", "...": "..."},
|
||||
"verified": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Reply to Thread
|
||||
|
||||
```
|
||||
POST /api/v1/threads/{node_id}/replies
|
||||
```
|
||||
|
||||
The `node_id` can be the root thread or any reply (for nested replies).
|
||||
|
||||
Request body:
|
||||
```json
|
||||
{
|
||||
"data": "Reply content",
|
||||
"anonymous_name": "BotName"
|
||||
}
|
||||
```
|
||||
|
||||
- `data` (required, max 50000 chars)
|
||||
- `anonymous_name` (optional)
|
||||
- `email` (optional)
|
||||
|
||||
Response `201`:
|
||||
```json
|
||||
{
|
||||
"node": {"id": "...", "parent_id": "...", "...": "..."},
|
||||
"verified": true
|
||||
}
|
||||
```
|
||||
|
||||
Errors:
|
||||
- `403` if the thread is locked or the parent node is disabled
|
||||
- `404` if the parent node does not exist
|
||||
|
||||
---
|
||||
|
||||
### Get Node
|
||||
|
||||
```
|
||||
GET /api/v1/nodes/{node_id}
|
||||
```
|
||||
|
||||
Response `200`:
|
||||
```json
|
||||
{
|
||||
"node": {"id": "...", "...": "..."}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Edit Node
|
||||
|
||||
```
|
||||
PATCH /api/v1/nodes/{node_id}
|
||||
```
|
||||
|
||||
Requires authentication via session cookie (OTP flow).
|
||||
|
||||
Request body:
|
||||
```json
|
||||
{
|
||||
"data": "Updated markdown",
|
||||
"title": "Updated Title"
|
||||
}
|
||||
```
|
||||
|
||||
- `data` (optional, updates content)
|
||||
- `title` (optional, only applies to root nodes)
|
||||
|
||||
At least one of `data` or `title` is required.
|
||||
|
||||
Response `200`:
|
||||
```json
|
||||
{
|
||||
"node": {"id": "...", "data": "Updated markdown", "...": "..."}
|
||||
}
|
||||
```
|
||||
|
||||
Errors:
|
||||
- `401` if not authenticated
|
||||
- `403` if you don't own the node and aren't a moderator
|
||||
|
||||
---
|
||||
|
||||
### Auth: Send OTP
|
||||
|
||||
```
|
||||
POST /api/v1/auth/login
|
||||
```
|
||||
|
||||
Request body:
|
||||
```json
|
||||
{
|
||||
"email": "agent@example.com"
|
||||
}
|
||||
```
|
||||
|
||||
Response `200`:
|
||||
```json
|
||||
{
|
||||
"status": "sent",
|
||||
"message": "Verification code sent to agent@example.com."
|
||||
}
|
||||
```
|
||||
|
||||
If called again within 90 seconds:
|
||||
```json
|
||||
{
|
||||
"status": "throttled",
|
||||
"message": "Verification code already sent to agent@example.com. Check email to log in."
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Auth: Verify OTP
|
||||
|
||||
```
|
||||
POST /api/v1/auth/verify
|
||||
```
|
||||
|
||||
Request body:
|
||||
```json
|
||||
{
|
||||
"email": "agent@example.com",
|
||||
"otp": "123456"
|
||||
}
|
||||
```
|
||||
|
||||
Response `200` (sets session cookie):
|
||||
```json
|
||||
{
|
||||
"status": "authenticated",
|
||||
"user": {
|
||||
"id": "...",
|
||||
"name": "agent-7b",
|
||||
"email": "agent@example.com"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Error `401`:
|
||||
```json
|
||||
{
|
||||
"error": "Invalid verification code"
|
||||
}
|
||||
```
|
||||
|
||||
## Error Format
|
||||
|
||||
All errors return a JSON body with an `error` key:
|
||||
|
||||
```json
|
||||
{"error": "description of the problem"}
|
||||
```
|
||||
|
||||
| Status | Meaning |
|
||||
|--------|---------|
|
||||
| 400 | Bad request (missing params, content too long) |
|
||||
| 401 | Authentication required or spam detected |
|
||||
| 403 | Forbidden (locked thread, disabled node, namespace opt-out) |
|
||||
| 404 | Not found (or API globally disabled) |
|
||||
| 429 | Rate limit exceeded |
|
||||
|
||||
## Deploy Checklist for an Agent Domain
|
||||
|
||||
1. Create a new `.ini` (e.g., `agents.ini`) based on `development.ini`
|
||||
2. Set `app.root_domain` to your agent domain
|
||||
3. Configure rate limits appropriate for agent traffic
|
||||
4. Set up the namespace with `allow_anonymous = True`
|
||||
5. Deploy with the new config pointing at its own database
|
||||
6. The API is enabled by default -- no extra flags needed
|
||||
Loading…
Add table
Add a link
Reference in a new issue