$ cli-blog docs

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.

ActionHTTP APINode SDKCLIDescription
ListGET /v1/mediablog.media.list() or .paginate()cli-blog media listPage through uploaded assets
UploadPOST /v1/mediablog.media.upload()cli-blog media uploadUpload multipart file data
RetrieveGET /v1/media/{id}blog.media.get()cli-blog media getGet one asset by ID
UpdatePOST /v1/media/{id}blog.media.update()cli-blog media updateChange editorial metadata with JSON
DeleteDELETE /v1/media/{id}blog.media.delete()cli-blog media deleteDelete 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/media
Choose an API key

Use 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" --json
Read the CLI guide

Use 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 guide

Use 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 skill

Upload 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." \
  --json

Use --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:

FieldDescription
featured_media_asset_idFeatured image for a post
media_asset_idsOrdered post images or file attachments
open_graph_media_asset_idOpen Graph social preview image
twitter_media_asset_idX/Twitter social preview image
avatar_media_idAuthor 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 --json

Update 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 --yes

Optimize 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=1200

Upscaling 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=1200

Use 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=north

Use 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:

FormatUse it for
avifPhotographs where modern browser support and smaller output matter
webpPhotographs, graphics, and transparency across modern browsers
jpegPhotographs that need broad compatibility and no transparency
pngGraphics, text, or transparency that should stay lossless
gifExisting 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=82

Add 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

ParameterAccepted valueResult
widthPixel integerResize to a target width while preserving aspect ratio
heightPixel integerResize to a target height while preserving aspect ratio
upscalingoff or resamplingControl whether output may exceed the source dimensions
aspect_ratioRatio such as 16:9, 4:3, or 1:1Crop to a repeatable proportion
cropwidth,height or width,height,x,yCenter-crop or crop from a source position
crop_gravityNamed anchorPosition a center crop around an edge or corner
focus_cropwidth,height,x,yKeep a known focal point inside the crop
formatavif, webp, jpeg, png, or gifEncode the transformed result in another format
qualityInteger from 0 to 100Control lossy compression quality
sharpentrue or falseIncrease edge definition after resizing
blurInteger from 0 to 100Apply 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

FieldDescription
id, object, organization_idStable resource identity
urlGenerated delivery URL
original_filenameFilename recorded during upload
alt_textAccessibility text for image-like assets
captionOptional reader-facing caption
mime_typeDetected media type
width, heightPixel dimensions when known
size_bytesUploaded asset size in bytes
metadataIntegration-defined JSON object, up to 32 KB when serialized
created_at, updated_atISO 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

StatusCauseResolution
400Missing file, malformed metadata, or mixed pagination modesCheck the named param and multipart fields
401Missing or invalid API keySend x-api-key with the private organization key
403Key lacks media permissionCreate or update a key with the required scope
404Media ID does not exist in the organizationConfirm the organization and asset ID
422Unsupported type, unsafe SVG, invalid file, or exceeded file ceilingFix or convert the file before uploading
429Organization storage allowance or request rate is exceededReview usage before retrying
502Managed storage could not complete the uploadRetry 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

Key type: Private key

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
x-api-key<token>

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

after?string

Opaque cursor returned by the previous media list page.

limit?|

Maximum media assets to return. Defaults to 20. The maximum accepted value is 100.

page?|

One-based exact page number. Supplying page selects numbered pagination and cannot be combined with after or limit.

per_page?|

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

Key type: Private key

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
x-api-key<token>

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

Key type: Private key

Retrieve one uploaded media asset.

Authorization

ApiKeyAuth
x-api-key<token>

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

id*string

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

Key type: Private key

Send a JSON body to update editorial media metadata. Storage fields are generated by upload and cannot be edited here.

Authorization

ApiKeyAuth
x-api-key<token>

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

id*string

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

Key type: Private key

Delete one uploaded media asset.

Authorization

ApiKeyAuth
x-api-key<token>

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

id*string

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"  }}

On this page