Skip to content

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

FieldTypeNotes
idstring (nanoid 21)Document ID
titlestringPriority: explicit param → frontmatter title: → first H1 → filename
filenamestringOriginal .md / .markdown filename
sizeBytesintUTF-8 byte length after image rewrite
wordCountintEstimated word count (Intl.Segmenter word)
imageCountintTotal image references (incl. external + skipped)
rewrittenCountintActual base64 → CDN uploads (post-dedup)
isPublicbooleanPublic docs allow anonymous /raw access
tagsstring[]Up to 10 tags, ≤ 32 chars each
hasInlineContentbooleantrue = stored in DB (≤ 256KB), false = R2
createdAt, updatedAtISO 8601

Quotas

Plandoc_countdoc_max_file_bytesdoc_max_images_per_doc
trial1001 MB30
pro10005 MB100
pro_plus100005 MB300

Endpoints

POST /v1/docs — upload

Terminal window
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![](data:image/png;base64,iVBOR...)",
"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

ParamTypeNotes
cursorstringPagination cursor from previous response
limitint1-50, default 20
qstringTitle fuzzy match (LIKE %q%)
tagstringSingle or comma-separated for OR (e.g. tag=ai,note)
isPublicboolFilter by visibility
sortenumcreated_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

HTTPcodeWhen
401UNAUTHORIZEDMissing / invalid Bearer for private docs
403QUOTA_EXCEEDEDdoc_count limit reached
404NOT_FOUNDDoc id not found
409DOC_HASH_DUPLICATESame content already uploaded (returns existing id)
422DOC_FILE_TOO_LARGEcontent > plan’s doc_max_file_bytes
422DOC_IMAGE_LIMIT_EXCEEDEDimages > plan’s doc_max_images_per_doc
422DOC_INVALID_PATTERN(admin) bad CDN whitelist pattern
502STORAGE_ERRORR2 / OSS unavailable

See also