Skip to content

Media ​

The brand's media library: images, carousels and video, in one feed.

GET /media/assets ​

Keyset page, newest first: {items, hasMore}.

paramvaluesnotes
kindimage,carousel,video (comma-separated)default all three
usageused / unusedfilters on the asset's usage count
statusready (default) / alla video still uploading, processing or failed shows only with all
uploaded_bymeyour own uploads only
idscomma-separated keyshydrate a known set, e.g. image:12,video:3, bypasses paging, max 100
limit1 to 100default 60
before / before_idkeyset cursoras returned on the previous page's last item

Each item carries a key ("image:123", "carousel:456", "video:45", all real ids of their own table), so every action route below takes {kind}/{id}.

An image:

json
{
  "key": "image:123",
  "id": 123,
  "kind": "image",
  "status": "ready",
  "url": "https://cdn.postlyra.com/…",
  "thumb_url": "https://cdn.postlyra.com/…",
  "name": "hero.jpg",
  "mime_type": "image/jpeg",
  "size_bytes": 812344,
  "width": 1080, "height": 1350,
  "duration_ms": null,
  "alt_text": "…", "caption": "…",
  "usage_count": 2,
  "uploaded_by": { "id": 7, "name": "Kristiyan" },
  "created_at": "2026-08-22T10:00:00+00:00",
  "slide_count": null
}

A carousel. slide_count is the one field only a carousel carries:

json
{
  "key": "carousel:456",
  "id": 456,
  "kind": "carousel",
  "status": "ready",
  "url": "https://cdn.postlyra.com/…/01.jpg",
  "thumb_url": "https://cdn.postlyra.com/…/01.jpg",
  "name": "Why most appeals fail",
  "mime_type": null,
  "size_bytes": 2048233,
  "width": 1080, "height": 1350,
  "duration_ms": null,
  "alt_text": null, "caption": null,
  "usage_count": 1,
  "uploaded_by": null,
  "created_at": "2026-09-01T08:00:00+00:00",
  "slide_count": 5
}

A carousel is one item no matter how many slides it has: a five-slide carousel is slide_count: 5, not five separate rows. status is always "ready" for an image or a carousel, even when nothing has rendered yet: url/thumb_url are its first rendered slide's picture, or "" in that case, and a client should show the carousel's name rather than a broken image. A rendered slide's own picture is never listed here as an image on its own; a picture placed as a slide, shown as-is rather than rendered, keeps kind: image and counts toward its own usage_count like any attachment.

Other endpoints ​

methodpathbody / result
POST/media/assetsmultipart file → 201, the asset. Images only (JPEG, PNG, WebP, GIF, up to 10 MB). Video has its own upload flow and isn't created here.
POST/media/assets/from-url{url} → 202 {queued: true}, fetched server-side, up to 25 MB, private and internal addresses refused
GET/media/assets/{kind}/{id}the asset
PATCH/media/assets/{kind}/{id}{alt_text?, caption?, name?} for an image; a carousel accepts {name?} only
DELETE/media/assets/{kind}/{id}{deleted: true}, or 422 {code: "media_referenced", blockers: [{post_id, title, reason}]} while a post still needs it
POST/media/assets/{kind}/{id}/new-post{post: {id, url}}, a draft in your name carrying the asset
GET/media/usage{used_bytes, limit_bytes}

Exceeding your storage allowance returns 422:

json
{ "message": "Storage limit reached. Free up space or upgrade your plan to upload more." }

Deleting is refused while a post needs the asset ​

A delete is refused whenever the asset is still needed: an unpublished post (draft, awaiting approval or approved), or a publication that is pending or about to go out. The blockers array names the posts, so you know what to detach first.

Once nothing needs it, deleting a carousel also deletes its own rendered pictures and its slides. A picture placed as a slide, rather than rendered, is never touched by that: it stays in the library as its own image.

Older endpoints ​

GET /media, POST /media and DELETE /media/{id} are the same upload/list/delete actions in an older shape: images only, no kind, no carousels. They still work, kept for existing integrations, but new integrations should use /media/assets above.

json
{
  "media": [
    {
      "id": 91,
      "file_name": "slide-1.jpg",
      "name": "slide-1",
      "mime_type": "image/jpeg",
      "size": 184320,
      "url": "https://cdn.postlyra.com/91/slide-1.jpg",
      "caption": null,
      "alt_text": null,
      "created_at": "2026-08-14T10:04:11.000000Z"
    }
  ]
}
bash
curl -X POST https://api.postlyra.com/media \
  -H "Authorization: Bearer $POSTLYRA_TOKEN" \
  -F "file=@slide-1.jpg"

The URLs are public ​

Media is world-readable

Media is served from a public CDN bucket. Anyone with the URL can fetch a file, with no authentication, and ids are sequential.

This is not a misconfiguration. Instagram and the other Graph APIs will only accept media as a public HTTPS URL, so the file has to be reachable for publishing to work at all.

Never upload anything you would not publish.

Video ​

Video does not go through these endpoints. Video assets upload directly from the browser to storage in parts, then get probed and validated against each platform's requirements. That flow lives in the draft composer in Studio. See Video uploads.

Postlyra, by MAVA Design