Media API

The Media API handles image and file uploads. Every upload gets indexed in the media table and (for images) auto-converted into AVIF and WebP variants plus multiple sizes so your front-end can serve <picture> elements without extra tooling. Endpoints live under /api/v1/media and require a Bearer token with the relevant media:* scope.

List media

GET /api/v1/media

Required scope: media:read.

Query parameters:

  • typeimage, video, document, audio
  • search — keyword search across filename, alt text, and caption
  • uploaded_by — user ID
  • page — 1-indexed (default 1)
  • per_page — max 100 (default 50)
  • order_byuploaded_at, filename, size (default uploaded_at)
curl -H "Authorization: Bearer YOUR_TOKEN" \
  "https://yoursite.com/api/v1/media?type=image&per_page=20"

Response:

{
    "data": [
        {
            "id": 45,
            "filename": "cover.jpg",
            "url": "https://yoursite.com/storage/uploads/2026/04/cover.jpg",
            "mime": "image/jpeg",
            "size_bytes": 248301,
            "width": 1600,
            "height": 900,
            "alt_text": "Plate of scrambled eggs",
            "caption": null,
            "variants": {
                "avif": "https://yoursite.com/storage/uploads/2026/04/cover.avif",
                "webp": "https://yoursite.com/storage/uploads/2026/04/cover.webp",
                "thumb_400": "https://yoursite.com/storage/uploads/2026/04/cover_400.jpg",
                "thumb_800": "https://yoursite.com/storage/uploads/2026/04/cover_800.jpg"
            },
            "uploaded_by": 1,
            "uploaded_at": "2026-04-10T08:55:00Z"
        }
    ],
    "pagination": { "page": 1, "per_page": 20, "total": 412, "total_pages": 21 }
}

Upload a file (multipart)

POST /api/v1/media
Content-Type: multipart/form-data

Required scope: media:write.

Form fields:

  • file (required) — the binary file
  • alt_text (optional) — accessibility text for images
  • caption (optional)
  • folder (optional) — virtual folder path, e.g., blog/2026/april

Example with curl:

curl -X POST https://yoursite.com/api/v1/media \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -F "file=@/path/to/cover.jpg" \
  -F "alt_text=Plate of scrambled eggs" \
  -F "folder=blog/breakfast"

Response: 201 Created with the full media object. For images, the AVIF and WebP variants plus resized thumbnails are generated asynchronously — the variants object is populated within a few seconds. Poll GET /api/v1/media/{id} if you need to wait for them before publishing.

Upload a remote image via URL

Convenience endpoint for pulling in external images (stock libraries, AI-generated URLs) without downloading them locally first:

POST /api/v1/media/fetch
Content-Type: application/json

Required scope: media:write.

{
    "url": "https://images.example.com/food/eggs-4k.jpg",
    "alt_text": "Plate of scrambled eggs",
    "folder": "blog/breakfast"
}

The server fetches the URL, validates the mime type against the allowlist, stores it locally, and generates the same variants as a normal upload. Response shape identical to the multipart endpoint.

Fails with 422 if the URL isn't reachable, returns a non-image mime type, or exceeds the max upload size.

Delete a media item

DELETE /api/v1/media/{id}

Required scope: media:write.

Removes the DB row, the original file, and all generated variants. If the media is currently used as a featured image or referenced in any post body, the delete is rejected with 409 Conflict and a list of referencing post IDs. Pass ?force=true to override (featured image references become null; body references become broken <img> tags).

Response: 204 No Content on success.

Limits

Max upload size

Hard limit: 50 MB per file. This is bounded by both PHP's upload_max_filesize / post_max_size and jekcms's own check. On shared hosting the practical limit is usually lower (check your host's PHP settings).

For video or archive uploads larger than 50 MB, use an S3-compatible external storage integration — the admin UI at Settings → Storage supports direct-to-bucket uploads that bypass the PHP process entirely.

Allowed mime types

By default the allowlist covers common web-safe types:

  • Images: image/jpeg, image/png, image/gif, image/webp, image/avif, image/svg+xml (SVG is sanitized on upload)
  • Video: video/mp4, video/webm, video/ogg
  • Audio: audio/mpeg, audio/mp4, audio/ogg, audio/wav
  • Documents: application/pdf

Non-allowlisted types are rejected with 422 unsupported_media_type. Admins can extend the list under Settings → Media → Allowed types — never add executable or archive types without serving them with Content-Disposition: attachment to prevent browser inline execution.

Variant generation

On image upload, jekcms queues background jobs that produce:

  • AVIF at 80% quality (best compression, modern browsers)
  • WebP at 85% quality (fallback)
  • Resized JPEGs at 400px and 800px wide (for thumbnails and cards)

All variants preserve EXIF orientation but strip GPS metadata (privacy). The originals are kept untouched. If the original is already AVIF or WebP, only the resized thumbnails are generated.

Variant generation runs on the media.variant_worker scheduled task — if it's not firing, check that cron is alive.

Error codes

  • 400 — missing file field or invalid form data
  • 401 — missing / invalid Bearer token
  • 403 — token lacks media:write scope
  • 404 — media ID not found (on DELETE / GET single)
  • 409 — cannot delete; media referenced elsewhere (pass force=true to override)
  • 413 — file exceeds 50 MB limit
  • 422 — unsupported mime type or invalid remote URL
  • 429 — rate limited

Be the first to know

New features, release notes & CMS guides — a couple of emails a month, no spam.