Skip to content

REST API reference

Base URL: https://api.canvasmesh.app

All /v1/* routes require an Authorization: Bearer <api-key> header. Share routes under /share/:token/* are public.

Content negotiation: every endpoint accepts both multipart/form-data (for file uploads) and application/json (for metadata-only changes or presigned uploads).

Create a canvas from a previously uploaded file. Upload the file first via the presigned flow, then reference its file_id here.

JSON body:

  • file_id (string, required) — from a successful /v1/upload/finalize call
  • title (string, required)
  • ext (string, required) — extension without dot, e.g. "md", "pdf"
  • renderer_id (string, optional) — override the default renderer (see renderer types)
  • permanent (boolean, default false) — skip the 24-hour auto-expiry

Response (201 Created):

{
"canvas_id": "cv_abc123",
"owner_user_id": "u_xyz",
"title": "Q2 Report",
"ext": "md",
"renderer_id": "rdr_builtin_markdown",
"visibility": "private",
"version": 1,
"created_at": "...",
"expires_at": "..."
}

To get the view URL, build it client-side as https://canvasmesh.app/c/<canvas_id> or use the SDK which returns it directly.

List canvases.

Query: visibility, limit (max 100, default 20), offset.

Response:

{ "canvases": [...], "limit": 20, "offset": 0 }

Get canvas metadata, including the current main item’s file and renderer.

Update a canvas. To replace file content, first upload via the presigned flow and pass the new file_id. Metadata-only updates don’t need a new file.

JSON body (all fields optional):

  • file_id (string) — new file to attach (creates a new version)
  • title (string)
  • visibility (private | public)
  • renderer_id (string)

Response: updated canvas object.

Delete the canvas and all its versions. Returns { "success": true }.

Convert a temporary canvas to permanent. Returns the updated canvas with expires_at: null.

Download the latest content. Responds 302 with a signed R2 URL in Location. Query ?dl=1 forces Content-Disposition: attachment.

List all versions.

Response:

{ "versions": [{ "version": 3, "size": 4211, "created_at": "..." }] }

Download a specific version (302 → signed URL).

Create a public share link.

JSON body:

  • expires_in (string, optional) — e.g. "7d", "24h", "30m"

Response (201):

{
"token": "sh_opaque_token",
"expires_at": "...",
"view_count": 0,
"created_at": "..."
}

Public share URL: https://canvasmesh.app/s/<token>.

Revoke all share links for this canvas.

Public endpoint — no auth. Returns 302 with a signed URL. Query ?dl=1 forces attachment. Optional header X-Share-Password.

Request a presigned R2 URL for direct upload.

JSON body:

{
"sha256": "<hex>",
"ext": "pdf",
"size": 1048576,
"mime": "application/pdf"
}

Response:

{
"file_id": "fl_xyz789",
"blob_key": "files/ab/...",
"upload_url": "<r2-presigned-url>",
"content_type": "application/pdf",
"expires_in": 3600,
"exists": false
}

If exists: true, skip the PUT and go straight to finalize.

Confirm a presigned upload completed.

JSON body: same shape as presign request.

Response:

{ "file": { "file_id": "fl_xyz789", "sha256": "...", "size": 1048576 }, "reused": false }

Unauthenticated health check. Returns { "ok": true }.

All errors return a JSON body:

{ "error": { "code": "canvas_not_found", "message": "..." } }
HTTPMeaning
400Invalid request
401Missing or invalid API key
403Key lacks permission for the resource
404Resource not found
409Conflict (e.g. duplicate share link)
413Payload too large — use presigned upload
429Rate limit exceeded
500Server error