Media
Media assets — images and other uploads — live on the site that owns the API key. Upload one, get its id, and reference that id from a post body image block. The site is always derived from the key, so a key can only ever see and touch its own assets.
- Base URL:
https://api.canverly.com - Auth: Bearer API key —
Authorization: Bearer ck_… - Rate limit: 60 req/min per key
| Method | Path | Scope |
|---|---|---|
GET |
/v1/media |
media:read |
POST |
/v1/media |
media:write |
GET |
/v1/media/{id} |
media:read |
DELETE |
/v1/media/{id} |
media:write |
GET /v1/media
Section titled “GET /v1/media”List the site’s assets, newest first. The listing is not cursor-paginated: you ask for a page size with limit.
| Parameter | Type | Required | Notes |
|---|---|---|---|
limit |
integer | no | 1..500, default 100. Out of range (or not an integer) returns 400 — it is never silently clamped. |
folder_id |
string | no | Restrict to one folder. |
tag_id |
string | no | Restrict to one tag. |
curl "https://api.canverly.com/v1/media?limit=20" \ -H "Authorization: Bearer ck_live_xxx"200 OK
Section titled “200 OK”{ "items": [ { "id": "019eedd0-1111-7000-8000-000000000001", "key": "sites/01KVPWYKT6AJDGG2EJN7DW4VT5/2026/08/hero.png", "mime": "image/png", "bytes": 184320, "width": 1600, "height": 900, "alt_text": "Dashboard screenshot", "hash": "9f2b1c7d5e4a3b8c6d0f1e2a3b4c5d6e7f8091a2b3c4d5e6f7081920a3b4c5d6", "status": "ready", "original_url": "https://cdn.example.com/sites/01KVPWYKT6AJDGG2EJN7DW4VT5/2026/08/hero.png" } ]}The response is always { "items": [...] } — the array is empty when the site has no assets (or none match the filters).
POST /v1/media
Section titled “POST /v1/media”Upload one asset as multipart/form-data. The file part must be named literally file — no other part name is read.
- Content-Type:
multipart/form-data(anything else returns400) - Max body: 8 MiB (over it returns
413) - Scope:
media:write
curl -X POST https://api.canverly.com/v1/media \ -H "Authorization: Bearer ck_live_xxx" \ -F "file=@./hero.png"202 Accepted
Section titled “202 Accepted”The asset is stored immediately, but its derived variants are generated asynchronously. That is why the call answers 202 and the asset comes back with status: "processing":
{ "id": "019eedd0-1111-7000-8000-000000000001", "key": "sites/01KVPWYKT6AJDGG2EJN7DW4VT5/2026/08/hero.png", "mime": "image/png", "bytes": 184320, "width": null, "height": null, "alt_text": null, "hash": "9f2b1c7d5e4a3b8c6d0f1e2a3b4c5d6e7f8091a2b3c4d5e6f7081920a3b4c5d6", "status": "processing", "original_url": null}Poll GET /v1/media/{id} until status flips to "ready". The id is usable as soon as you have it — you do not have to wait for "ready" to record it.
Upload-specific failures
Section titled “Upload-specific failures”-
Not multipart — sending JSON (or no
Content-Type) returns400:{ "error": { "code": "bad_request", "message": "expected multipart/form-data body with a `file` field" } } -
Body over 8 MiB —
413. This is the same 8 MiB ceiling the rest of the public edge enforces. -
Unsupported file type —
415, relayed from the media service. -
Key without authorship context — a key minted before author/organization context existed cannot upload. It returns
422:{ "error": { "code": "unprocessable", "message": "this api key has no org context; re-create it in Configurações → Integrações API" } }The fix is on the owner’s side: delete the key and generate a new one in Configurações → Integrações API (see Authentication).
GET /v1/media/{id}
Section titled “GET /v1/media/{id}”Read one asset. Use it to poll an upload until it is "ready".
curl https://api.canverly.com/v1/media/019eedd0-1111-7000-8000-000000000001 \ -H "Authorization: Bearer ck_live_xxx"200 OK
Section titled “200 OK”{ "id": "019eedd0-1111-7000-8000-000000000001", "key": "sites/01KVPWYKT6AJDGG2EJN7DW4VT5/2026/08/hero.png", "mime": "image/png", "bytes": 184320, "width": 1600, "height": 900, "alt_text": "Dashboard screenshot", "hash": "9f2b1c7d5e4a3b8c6d0f1e2a3b4c5d6e7f8091a2b3c4d5e6f7081920a3b4c5d6", "status": "ready", "original_url": "https://cdn.example.com/sites/01KVPWYKT6AJDGG2EJN7DW4VT5/2026/08/hero.png"}An id that belongs to another site returns 404, exactly like an id that does not exist — existence is never disclosed:
{ "error": { "code": "not_found", "message": "media asset not found" } }DELETE /v1/media/{id}
Section titled “DELETE /v1/media/{id}”Soft-delete an asset.
curl -X DELETE https://api.canverly.com/v1/media/019eedd0-1111-7000-8000-000000000001 \ -H "Authorization: Bearer ck_live_xxx" \ -i204 No Content
Section titled “204 No Content”No response body. As with GET, an id from another site returns 404 and not 403.
The asset object
Section titled “The asset object”| Field | Type | Notes |
|---|---|---|
id |
string | Asset identifier — this is what you put in a post’s image block. |
key |
string | Object path in storage. |
mime |
string | Detected MIME type, e.g. image/png. |
bytes |
integer | Stored size in bytes. |
width |
integer | null | Pixel width; null when unknown (typically while status is processing, or for non-image types). |
height |
integer | null | Pixel height; same caveat as width. |
alt_text |
string | null | Alternative text, when set. |
hash |
string | Content hash of the stored object. |
status |
string | "processing" while variants are being generated, "ready" afterwards. |
original_url |
string (URI) | null | URL of the original object, when available. |
Using an asset in a post
Section titled “Using an asset in a post”An image block in a post body can carry the asset id as attrs.media_id instead of a URL. When the post is served, an image recorded as a media id comes back with the URL already resolved in content_html; if the asset has been deleted or is unavailable, the block is omitted rather than rendered as a broken <img>.
{ "v": 1, "blocks": [ { "id": "img1", "type": "image", "attrs": { "media_id": "019eedd0-1111-7000-8000-000000000001", "alt": "Dashboard screenshot" } } ]}See Content blocks for the block document format and POST /v1/posts for creating the post itself.
Errors
Section titled “Errors”Envelope: { "error": { "code", "message" } }. Full table in Errors & rate limits.
| Status | error.code |
When |
|---|---|---|
400 |
bad_request |
limit not an integer or outside 1..500 (list); body not multipart/form-data, or no file part (upload). |
401 |
unauthorized |
Missing, malformed, unknown or revoked key — including a pk_… public read key. |
403 |
forbidden |
Key lacks media:read (list/get) or media:write (upload/delete). |
404 |
not_found |
No such asset on this site — same answer for another site’s id. |
413 |
— | Upload body over 8 MiB. |
415 |
— | Unsupported media type, relayed from the media service. |
422 |
unprocessable |
Upload with a key that has no organization context — re-create the key. |
429 |
rate_limited |
Over 60 req/min — honor Retry-After. |
5xx |
upstream |
Transient; retry with backoff. Internal detail is redacted from the message. |