Media
The brand's media library: images, carousels and video, in one feed.
GET /media/assets
Keyset page, newest first: {items, hasMore}.
| param | values | notes |
|---|---|---|
kind | image,carousel,video (comma-separated) | default all three |
usage | used / unused | filters on the asset's usage count |
status | ready (default) / all | a video still uploading, processing or failed shows only with all |
uploaded_by | me | your own uploads only |
ids | comma-separated keys | hydrate a known set, e.g. image:12,video:3, bypasses paging, max 100 |
limit | 1 to 100 | default 60 |
before / before_id | keyset cursor | as 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:
{
"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:
{
"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
| method | path | body / result |
|---|---|---|
POST | /media/assets | multipart 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:
{ "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.
{
"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"
}
]
}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.