Markdown Documents API
Picora v0.15+ supports Markdown documents as a first-class resource type alongside images, videos and audio. This page covers the REST API; for AI-driven workflows see the MCP upload_doc 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 .md / .markdown filename |
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 | 1000 | 5 MB | 100 |
| pro_plus | 10000 | 5 MB | 300 |
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).
Error codes
| HTTP | code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing / invalid Bearer for private docs |
| 403 | QUOTA_EXCEEDED | doc_count limit reached |
| 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 integration —
upload_doctool walkthrough