Documents API
Picora supports documents as a first-class resource type alongside images, videos and audio. Two markup languages share this one API (v0.82.0+):
- Markdown —
.md/.markdown(v0.15.0+) - Typst —
.typ(v0.82.0+)
The format is decided by the filename extension and reported back in the format field. Markdown-specific processing (embedded base64 image rewriting, frontmatter parsing, excerpt/cover extraction) applies to Markdown only; Typst source is stored byte-for-byte.
This page covers the REST API; for AI-driven workflows see the picora.upload_doc MCP tool.
Resource model
| Field | Type | Notes |
|---|---|---|
id | string (nanoid 21) | Document ID |
title | string | Priority: explicit param → frontmatter title: → first H1 → filename |
filename | string | Original filename, incl. extension (.md / .markdown / .typ) |
format | string | markdown or typst (v0.82.0+). Derived from the extension; documents created earlier report markdown |
sizeBytes | int | UTF-8 byte length after image rewrite |
wordCount | int | Estimated word count (Intl.Segmenter word) |
imageCount | int | Total image references (incl. external + skipped) |
rewrittenCount | int | Actual base64 → CDN uploads (post-dedup) |
isPublic | boolean | Public docs allow anonymous /raw access |
tags | string[] | Up to 10 tags, ≤ 32 chars each |
hasInlineContent | boolean | true = stored in DB (≤ 256KB), false = R2 |
createdAt, updatedAt | ISO 8601 |
Quotas
| Plan | doc_count | doc_max_file_bytes | doc_max_images_per_doc |
|---|---|---|---|
| trial | 100 | 1 MB | 30 |
| pro | -1 (unlimited) | 5 MB | 100 |
| pro_plus | -1 (unlimited) | 5 MB | 300 |
doc_count is returned as -1 in planLimits.docCount (GET /v1/user/me/usage) when the plan has no document-count cap. Check for -1 before rendering a usage ratio.
Endpoints
POST /v1/docs — upload
curl -X POST https://api.picora.me/v1/docs \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "filename": "README.md", "content": "# Hello\n", "tags": ["readme"], "isPublic": false, "rewriteImages": true }'Response (201 on new, 200 on duplicate):
{ "success": true, "data": { "id": "abc123XYZ_ki1234567890", "title": "Hello", "imageCount": 1, "rewrittenCount": 1, "failedCount": 0, "failures": [], "warnings": [], "duplicate": false, "hasInlineContent": true, "createdAt": "2026-04-28T08:00:00.000Z" }}Inline image rewriting: data:image/{png,jpeg,webp,gif,svg+xml};base64,... URLs are decoded, optionally SVG-sanitized, and uploaded to your image quota. The markdown URL is replaced with https://media.picora.me/{nanoid}.{ext}.
SHA-256 idempotency: Re-uploading identical content returns the existing doc id with duplicate: true and does NOT consume quota.
GET /v1/docs — list
| Param | Type | Notes |
|---|---|---|
cursor | string | Pagination cursor from previous response |
limit | int | 1-50, default 20 |
q | string | Title fuzzy match (LIKE %q%) |
tag | string | Single or comma-separated for OR (e.g. tag=ai,note) |
isPublic | bool | Filter by visibility |
sort | enum | created_desc (default), created_asc, updated_desc, updated_asc |
GET /v1/docs/:id — metadata
Returns metadata only. Use :id/raw for full content.
GET /v1/docs/:id/raw — raw markdown
Returns Content-Type: text/markdown; charset=utf-8.
- Public docs: anonymous access,
Cache-Control: public, max-age=300 - Private docs: requires
Authorization: Bearer ...,no-store
PATCH /v1/docs/:id — update metadata
{ "title": "...", "isPublic": true, "tags": ["ai"] }Content updates: not supported — re-POST and DELETE old.
DELETE /v1/docs/:id and DELETE /v1/docs (batch)
Single delete or body { ids: string[] } (max 50). Embedded images are NOT cascaded (orphan cleanup is future). A hard delete also removes the document’s version history.
Version history
Optional per-user feature (default off) that keeps a rolling history of knowledge-base documents. Enable it and set the retention count via PATCH /v1/user/me (docVersioningEnabled, docVersioningMax 1–500). When on, any content replacement at the same (kbId, relativePath) saves the previous content as a version; the oldest is pruned once docVersioningMax is exceeded. History storage is reported under docs.revisionCount / docs.revisionBytes in GET /v1/user/me/usage.
GET /v1/docs/:id/revisions — list versions
Returns versions newest-first plus total size. Read-only — available even when a plan has expired.
{ "success": true, "data": { "revisions": [ { "id": "Rk9MR2PQ7vB01234567ab", "revNumber": 3, "sizeBytes": 20480, "origin": "sync", "sourceHash": "e3b0c442…", "createdAt": "2026-07-10T08:00:00.000Z" } ], "totalBytes": 61440} }origin is upload (direct re-upload) · sync (editor/KB sync) · restore (produced by a restore).
GET /v1/docs/:id/revisions/:revId — read a version’s content
Returns the version metadata plus its full content (Markdown or Typst source). Read-only.
POST /v1/docs/:id/revisions/:revId/restore — restore
Rewrites the document with the version’s content. The current content is first snapshotted as a new version (never destructive). Requires an active plan (returns 403 PLAN_INACTIVE otherwise). If the restored content equals the current content, responds with duplicate: true and no side effects.
DELETE /v1/user/me/doc-revisions — clear all history
Deletes every version for the current user to reclaim storage. Processes up to 200 per call — repeat while hasMore is true. Allowed regardless of plan status (reclaiming your own space is always permitted).
{ "success": true, "data": { "deleted": 37, "freedBytes": 8388608, "hasMore": false } }Error codes
| HTTP | code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing / invalid Bearer for private docs |
| 403 | PLAN_INACTIVE | Restore attempted without an active plan |
| 403 | QUOTA_EXCEEDED | doc_count limit reached |
| 404 | DOC_REVISION_NOT_FOUND | Version id not found / not owned |
| 404 | NOT_FOUND | Doc id not found |
| 409 | DOC_HASH_DUPLICATE | Same content already uploaded (returns existing id) |
| 422 | DOC_FILE_TOO_LARGE | content > plan’s doc_max_file_bytes |
| 422 | DOC_IMAGE_LIMIT_EXCEEDED | images > plan’s doc_max_images_per_doc |
| 422 | DOC_INVALID_PATTERN | (admin) bad CDN whitelist pattern |
| 502 | STORAGE_ERROR | R2 / OSS unavailable |
See also
- Markdown hosting guide — usage scenarios + AI workflow examples
- MCP — the
picora.upload_doctool and how to connect a client