Media
Upload and manage images, documents, and video assets for posts and author profiles.
Media is upload-first. Upload a file, then assign the returned media ID to posts, author avatars, featured images, social previews, or ordered attachments.
Choose a surface
All media actions require a private key and media permission. Compare the API, Node SDK, CLI, and agent skill.
| Action | HTTP API | Node SDK | CLI | Description |
|---|---|---|---|---|
| List | GET /v1/media | blog.media.list() or .paginate() | cli-blog media list | Page through uploaded assets |
| Upload | POST /v1/media | blog.media.upload() | cli-blog media upload | Upload multipart file data |
| Retrieve | GET /v1/media/{id} | blog.media.get() | cli-blog media get | Get one asset by ID |
| Update | POST /v1/media/{id} | blog.media.update() | cli-blog media update | Change editorial metadata with JSON |
| Delete | DELETE /v1/media/{id} | blog.media.delete() | cli-blog media delete | Delete one media asset |
Direct upload is the content API's multipart exception. Post and category or tag writes use JSON. Media metadata updates also use JSON.
Use the REST API
Call the resource from any technology that can send HTTP requests. Use a public key for allowed delivery reads and a private key for content changes.
POST https://api.cli-blog.com/v1/mediaChoose an API keyUse the CLI
Use the CLI for local work, continuous integration, and JSON automation. Configure a narrowly scoped key before running content-changing commands.
cli-blog media upload ./golden-gate.jpg --alt-text "Golden Gate Bridge" --jsonRead the CLI guideUse the Node SDK
Use the Node SDK in Node.js 20+ servers, scripts, continuous integration, and trusted agent environments. Its methods map to the same resource actions.
await blog.media.upload({ file, filename, alt_text })Read the Node SDK guideUse the agent skill
Give the agent the Cli Blog skill and a result-focused instruction. The skill helps it choose the API, CLI, or SDK and keeps publishing behind an approval gate.
Upload the approved Golden Gate image with useful alt text and return its media ID without publishing a post.Install the agent skillUpload a media asset
Send the file as the file multipart field. The API derives its delivery URL, MIME type, dimensions, and byte size.
curl "https://api.cli-blog.com/v1/media" \
--header "x-api-key: $CLI_BLOG_PRIVATE_API_KEY" \
--form "file=@./release-workflow.png" \
--form "alt_text=Release workflow diagram with draft, review, and publish steps" \
--form "caption=The editorial workflow used by the Acme engineering team."The response contains the ID used by other resources:
{
"id": "media_c81f20",
"object": "media_asset",
"organization_id": "org_demo_acme",
"url": "https://cdn.cli-blog.example/o/org_demo_acme/release-workflow.png",
"original_filename": "release-workflow.png",
"alt_text": "Release workflow diagram with draft, review, and publish steps",
"caption": "The editorial workflow used by the Acme engineering team.",
"mime_type": "image/png",
"width": 1600,
"height": 900,
"size_bytes": 284116,
"metadata": {},
"created_at": "2026-07-11T17:05:00.000Z",
"updated_at": "2026-07-11T17:05:00.000Z"
}The Node SDK accepts a web-standard Blob in Node.js 20 or newer:
import { readFile } from "node:fs/promises";
import { CliBlog } from "@cli-blog/node";
const blog = new CliBlog({
apiKey: process.env.CLI_BLOG_PRIVATE_API_KEY!,
apiUrl: "https://api.cli-blog.com",
});
const bytes = await readFile("./release-workflow.png");
const file = new Blob([bytes], { type: "image/png" });
const media = await blog.media.upload({
file,
filename: "release-workflow.png",
alt_text: "Release workflow diagram with draft, review, and publish steps",
});The CLI reads the file path and infers common MIME types from its extension:
cli-blog media upload \
--file ./release-workflow.png \
--alt-text "Release workflow diagram with draft, review, and publish steps" \
--caption "The editorial workflow used by the Acme engineering team." \
--jsonUse --content-type when the filename has no recognized extension.
Assign media to content
Keep media IDs in content records. Each relationship has a distinct purpose:
| Field | Description |
|---|---|
featured_media_asset_id | Featured image for a post |
media_asset_ids | Ordered post images or file attachments |
open_graph_media_asset_id | Open Graph social preview image |
twitter_media_asset_id | X/Twitter social preview image |
avatar_media_id | Author profile avatar |
Unlinking an attachment does not delete the asset. Delete the media record only when no published view needs it.
List and retrieve media
Cursor pagination is the preferred list mode. Set limit up to 100, then pass next_cursor as after. Use numbered pagination only when a media-library interface requires exact totals. See Pagination.
const firstPage = await blog.media.list({ limit: 50 });
const nextPage = firstPage.next_cursor
? await blog.media.list({ limit: 50, after: firstPage.next_cursor })
: null;
const cover = await blog.media.get("media_c81f20");cli-blog media list --limit 50 --json
cli-blog media get media_c81f20 --jsonUpdate metadata and delete media
Only editorial metadata is editable. The upload controls storage fields such as url, mime_type, dimensions, and byte size.
await blog.media.update("media_c81f20", {
alt_text: "Three-step release workflow from draft to published post",
caption: "Draft, review, then publish.",
metadata: { source: "launch-guide" },
});
await blog.media.delete("media_c81f20");cli-blog media update media_c81f20 \
--alt-text "Three-step release workflow from draft to published post" \
--metadata '{"source":"launch-guide"}' \
--json
cli-blog media delete media_c81f20 --yesOptimize images for delivery
Cli Blog keeps the uploaded source and returns a provider-neutral delivery URL. Add transformation parameters to that URL when a page needs a different size, crop, format, or compression level. You keep one media ID and avoid uploading a thumbnail for every layout.
The delivery layer transforms the source before serving the requested variant. Crop operations run first, resizing runs second, and visual filters run after the output dimensions are known.
Resize without changing the source
Set width or height in pixels. If you provide one dimension, the other dimension follows the original aspect ratio. If you provide both, the result fits within those bounds without stretching the image.
https://cdn.cli-blog.example/o/org_demo_acme/release-workflow.png?width=1200Upscaling is off by default. Add upscaling=resampling only when the design requires an output larger than the uploaded source. Upscaling cannot restore detail that the source does not contain.
Crop for a specific placement
Use aspect_ratio for repeatable blog cards and hero images:
https://cdn.cli-blog.example/o/org_demo_acme/release-workflow.png?aspect_ratio=16:9&width=1200Use crop=width,height for an exact center crop. Add crop_gravity when the subject should stay near an edge. Supported anchors include center, north, south, east, west, northeast, northwest, southeast, and southwest.
https://cdn.cli-blog.example/o/org_demo_acme/release-workflow.png?crop=1200,630&crop_gravity=northUse focus_crop=width,height,x,y when you know the subject's focal point. The x and y values can be source-image pixels or relative values from 0.0 to 1.0.
Choose an output format
Set format when the page needs a specific encoded result:
| Format | Use it for |
|---|---|
avif | Photographs where modern browser support and smaller output matter |
webp | Photographs, graphics, and transparency across modern browsers |
jpeg | Photographs that need broad compatibility and no transparency |
png | Graphics, text, or transparency that should stay lossless |
gif | Existing lightweight animations or limited-color graphics |
Converting a transparent image to JPEG removes transparency. Use PNG, WebP, or AVIF when the output must preserve it.
Balance quality and detail
Set quality from 0 to 100 for JPEG, WebP, and AVIF. Higher values preserve more detail and produce larger responses. PNG does not use lossy quality compression.
Start photographs between 75 and 85, then inspect the actual page at its intended dimensions. Thumbnails can often use a lower value than screenshots or graphics with sharp text. Avoid one quality value for every asset type.
https://cdn.cli-blog.example/o/org_demo_acme/release-workflow.png?aspect_ratio=16:9&width=1200&format=webp&quality=82Add sharpen=true after resizing when a smaller image needs clearer edges. Use blur from 0 to 100 only for intentional visual treatment or obscuring sensitive detail.
Build responsive image variants
Use the same source URL in srcset and change only the requested width. The browser chooses a variant based on the rendered size and device pixel ratio.
<img
src="https://cdn.cli-blog.example/o/org_demo_acme/release-workflow.png?width=960&format=webp&quality=82"
srcset="
https://cdn.cli-blog.example/o/org_demo_acme/release-workflow.png?width=640&format=webp&quality=82 640w,
https://cdn.cli-blog.example/o/org_demo_acme/release-workflow.png?width=960&format=webp&quality=82 960w,
https://cdn.cli-blog.example/o/org_demo_acme/release-workflow.png?width=1600&format=webp&quality=82 1600w
"
sizes="(min-width: 64rem) 50vw, 100vw"
alt="Release workflow from draft to published post"
/>Keep the number of width variants bounded. Reuse a small set that matches your layout breakpoints instead of generating arbitrary widths for every request.
Transformation parameters
| Parameter | Accepted value | Result |
|---|---|---|
width | Pixel integer | Resize to a target width while preserving aspect ratio |
height | Pixel integer | Resize to a target height while preserving aspect ratio |
upscaling | off or resampling | Control whether output may exceed the source dimensions |
aspect_ratio | Ratio such as 16:9, 4:3, or 1:1 | Crop to a repeatable proportion |
crop | width,height or width,height,x,y | Center-crop or crop from a source position |
crop_gravity | Named anchor | Position a center crop around an edge or corner |
focus_crop | width,height,x,y | Keep a known focal point inside the crop |
format | avif, webp, jpeg, png, or gif | Encode the transformed result in another format |
quality | Integer from 0 to 100 | Control lossy compression quality |
sharpen | true or false | Increase edge definition after resizing |
blur | Integer from 0 to 100 | Apply an intentional blur effect |
Always keep meaningful alt_text on the media record and on the rendered image. A transformed variant changes pixels, not the accessibility description or media ID.
Media fields and limits
| Field | Description |
|---|---|
id, object, organization_id | Stable resource identity |
url | Generated delivery URL |
original_filename | Filename recorded during upload |
alt_text | Accessibility text for image-like assets |
caption | Optional reader-facing caption |
mime_type | Detected media type |
width, height | Pixel dimensions when known |
size_bytes | Uploaded asset size in bytes |
metadata | Integration-defined JSON object, up to 32 KB when serialized |
created_at, updated_at | ISO timestamps returned by the API |
The API accepts AVIF, GIF, JPEG, PNG, SVG, WebP, PDF, DOC, DOCX, KEY, PPT, PPTX, MP4, and WebM. SVG files have a 5 MB ceiling and must contain safe static vector markup. Other supported files have a 100 MB ceiling.
Common errors
| Status | Cause | Resolution |
|---|---|---|
400 | Missing file, malformed metadata, or mixed pagination modes | Check the named param and multipart fields |
401 | Missing or invalid API key | Send x-api-key with the private organization key |
403 | Key lacks media permission | Create or update a key with the required scope |
404 | Media ID does not exist in the organization | Confirm the organization and asset ID |
422 | Unsupported type, unsafe SVG, invalid file, or exceeded file ceiling | Fix or convert the file before uploading |
429 | Organization storage allowance or request rate is exceeded | Review usage before retrying |
502 | Managed storage could not complete the upload | Retry after the storage service recovers |
Complete API operations
The operation reference lists every multipart field, JSON field, response field, media type, size limit, and error schema. Use Try it to open the request runner.
List media
List uploaded media assets. Cursor pagination is the default; page and per_page opt into exact numbered pagination and cannot be combined with after or limit.
Authorization
ApiKeyAuth Organization API key. Each key selects exactly one organization; do not send an organization ID separately. Public keys use the cli_blog_pk_ prefix and are intended for published-content delivery reads. Private keys use the cli_blog_sk_ prefix, belong only in trusted environments, and are required for write, publish, delete, and editorial-state workflows when their scopes allow it.
In: header
Query Parameters
Opaque cursor returned by the previous media list page.
Maximum media assets to return. Defaults to 20. The maximum accepted value is 100.
One-based exact page number. Supplying page selects numbered pagination and cannot be combined with after or limit.
Items per numbered page. Requires page, defaults to 20, and cannot be combined with after or limit.
Response Body
application/json
application/json
application/json
application/json
application/json
application/json
curl -X GET "https://example.com/v1/media"{ "object": "list", "has_more": true, "next_cursor": "string", "page": 0, "per_page": 0, "total_items": 0, "total_pages": 0, "data": [ { "id": "string", "object": "media_asset", "organization_id": "string", "url": "string", "original_filename": "string", "alt_text": "string", "caption": "string", "mime_type": "string", "width": 0, "height": 0, "size_bytes": 0, "metadata": null, "created_at": "string", "updated_at": "string" } ]}{ "error": { "code": "invalid_request", "message": "string", "param": "string" }}{ "error": { "code": "invalid_request", "message": "string", "param": "string" }}{ "error": { "code": "invalid_request", "message": "string", "param": "string" }}{ "error": { "code": "invalid_request", "message": "string", "param": "string" }}{ "error": { "code": "invalid_request", "message": "string", "param": "string" }}Upload media
Send multipart file data to upload a supported blog media file into service-managed storage and create the media record with generated URL and storage metadata. SVG uploads are limited to safe static vector markup and reject scripts, embedded HTML, animation, external references, and unsafe CSS. This file route is the content API exception to JSON write bodies.
Authorization
ApiKeyAuth Organization API key. Each key selects exactly one organization; do not send an organization ID separately. Public keys use the cli_blog_pk_ prefix and are intended for published-content delivery reads. Private keys use the cli_blog_sk_ prefix, belong only in trusted environments, and are required for write, publish, delete, and editorial-state workflows when their scopes allow it.
In: header
Request Body
multipart/form-data
TypeScript Definitions
Use the request body type in TypeScript.
Response Body
application/json
application/json
application/json
application/json
application/json
application/json
application/json
application/json
curl -X POST "https://example.com/v1/media" \ -F file="File"{ "id": "string", "object": "media_asset", "organization_id": "string", "url": "string", "original_filename": "string", "alt_text": "string", "caption": "string", "mime_type": "string", "width": 0, "height": 0, "size_bytes": 0, "metadata": null, "created_at": "string", "updated_at": "string"}{ "error": { "code": "invalid_request", "message": "string", "param": "string" }}{ "error": { "code": "invalid_request", "message": "string", "param": "string" }}{ "error": { "code": "invalid_request", "message": "string", "param": "string" }}{ "error": { "code": "invalid_request", "message": "string", "param": "string" }}{ "error": { "code": "invalid_request", "message": "string", "param": "string" }}{ "error": { "code": "invalid_request", "message": "string", "param": "string" }}{ "error": { "code": "invalid_request", "message": "string", "param": "string" }}Retrieve media
Retrieve one uploaded media asset.
Authorization
ApiKeyAuth Organization API key. Each key selects exactly one organization; do not send an organization ID separately. Public keys use the cli_blog_pk_ prefix and are intended for published-content delivery reads. Private keys use the cli_blog_sk_ prefix, belong only in trusted environments, and are required for write, publish, delete, and editorial-state workflows when their scopes allow it.
In: header
Path Parameters
Response Body
application/json
application/json
application/json
application/json
application/json
curl -X GET "https://example.com/v1/media/string"{ "id": "string", "object": "media_asset", "organization_id": "string", "url": "string", "original_filename": "string", "alt_text": "string", "caption": "string", "mime_type": "string", "width": 0, "height": 0, "size_bytes": 0, "metadata": null, "created_at": "string", "updated_at": "string"}{ "error": { "code": "invalid_request", "message": "string", "param": "string" }}{ "error": { "code": "invalid_request", "message": "string", "param": "string" }}{ "error": { "code": "invalid_request", "message": "string", "param": "string" }}{ "error": { "code": "invalid_request", "message": "string", "param": "string" }}Update media metadata
Send a JSON body to update editorial media metadata. Storage fields are generated by upload and cannot be edited here.
Authorization
ApiKeyAuth Organization API key. Each key selects exactly one organization; do not send an organization ID separately. Public keys use the cli_blog_pk_ prefix and are intended for published-content delivery reads. Private keys use the cli_blog_sk_ prefix, belong only in trusted environments, and are required for write, publish, delete, and editorial-state workflows when their scopes allow it.
In: header
Path Parameters
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Response Body
application/json
application/json
application/json
application/json
application/json
curl -X POST "https://example.com/v1/media/string" \ -H "Content-Type: application/json" \ -d '{}'{ "id": "string", "object": "media_asset", "organization_id": "string", "url": "string", "original_filename": "string", "alt_text": "string", "caption": "string", "mime_type": "string", "width": 0, "height": 0, "size_bytes": 0, "metadata": null, "created_at": "string", "updated_at": "string"}{ "error": { "code": "invalid_request", "message": "string", "param": "string" }}{ "error": { "code": "invalid_request", "message": "string", "param": "string" }}{ "error": { "code": "invalid_request", "message": "string", "param": "string" }}{ "error": { "code": "invalid_request", "message": "string", "param": "string" }}Delete media
Delete one uploaded media asset.
Authorization
ApiKeyAuth Organization API key. Each key selects exactly one organization; do not send an organization ID separately. Public keys use the cli_blog_pk_ prefix and are intended for published-content delivery reads. Private keys use the cli_blog_sk_ prefix, belong only in trusted environments, and are required for write, publish, delete, and editorial-state workflows when their scopes allow it.
In: header
Path Parameters
Response Body
application/json
application/json
application/json
application/json
application/json
curl -X DELETE "https://example.com/v1/media/string"{ "deleted": true, "id": "string"}{ "error": { "code": "invalid_request", "message": "string", "param": "string" }}{ "error": { "code": "invalid_request", "message": "string", "param": "string" }}{ "error": { "code": "invalid_request", "message": "string", "param": "string" }}{ "error": { "code": "invalid_request", "message": "string", "param": "string" }}