Skip to content
Wahlu API

Media

Upload, import and list media, and explicitly read its processing readiness.

The released Media API has five operations: create a caller-owned upload session, import a public image or video URL, list recent media, explicitly read readiness, and create a reviewed repair derivative for a supported incompatible format. Wahlu returns opaque delivery URLs underhttps://media.wahlu.com/assets/…, never a storage-provider URL.

Create an upload session

POST/v1/brands/:brand_id/media/upload-sessions

Requires media:write. Send the filename, supported content type, exact byte size, SHA-256 content hash, and an idempotency key. Wahlu returns a short-lived scoped PUT capability; local clients can stream the caller-owned bytes without making them public first. Hosted MCP offers upload_media; local stdio also offers file upload.

Import media from a URL

POST/v1/brands/:brand_id/media/imports

Requires media:write. The remote server must expose a supported image or video over HTTP or HTTPS with an exactContent-Length. This operation starts processing and returns a stable Media ID plus a resolved read link.

Send either an Idempotency-Key header or anidempotency_key body field for each logical import. If both are sent, they must match. A new import returns 201; an exact replay returns 200 with Idempotency-Replayed: true and does not fetch the remote URL again. Reusing the key with different input returns 409.

Request body

ParameterTypeDescription
url (required)stringPublic HTTP or HTTPS image or video URL.
filenamestringOptional filename override, up to 500 characters.
folder_idstringOptional existing media folder ID.
idempotency_keystringBody alternative to Idempotency-Key. If both are sent, they must match.
curl
curl -X POST https://api.wahlu.com/v1/brands/brand_01k0acme/media/imports \
  -H "Authorization: Bearer $WAHLU_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: import-autumn-hero-v1" \
  -d '{
    "url": "https://assets.example.com/campaign/hero.jpg",
    "filename": "hero.jpg"
  }'
201 response
{
  "success": true,
  "data": {
    "id": "media_01k0hero",
    "status": "processing",
    "replayed": false,
    "download_url": "https://media.wahlu.com/assets/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
    "links": {
      "self": {
        "href": "/v1/brands/brand_01k0acme/media/media_01k0hero",
        "method": "GET",
        "required_scopes": ["media:read"]
      }
    }
  },
  "meta": { "request_id": "req_media_imported_01k0" }
}

Create a reviewed repair derivative

POST/v1/brands/:brand_id/media/:media_id/repair-derivatives

Requires media:write. Use an exact repair_options entry returned by content preflight. Explicitly send its target integration, platform, post type, mode, option ID, and policy version with a caller-owned idempotency key. Wahlu recomputes the option against the current source hash, then creates a new immutable Media item; it never overwrites the source or silently changes the draft's selected media.

An exact replay returns the same derivative with 200. Changed parameters under the same key return 409. Stale options and transforms outside the versioned loss limits fail closed. To publish the reviewed derivative, explicitly place its returned Media ID in the target platform's media_ids and run preflight again.

Create the selected derivative
curl -X POST https://api.wahlu.com/v1/brands/brand_01k0acme/media/media_01k0hero/repair-derivatives \
  -H "Authorization: Bearer $WAHLU_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: repair-autumn-grid-v1" \
  -d '{
    "integration_id": "integration_01k0instagram",
    "platform": "instagram",
    "post_type": "GRID_POST",
    "mode": "centre_crop",
    "repair_option_id": "repair_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
    "policy_version": "2026-08-05.1"
  }'

List media

GET/v1/brands/:brand_id/media

Requires media:read. Returns one bounded page of the brand's media, newest first. Each item has the same shape as a single Media read, includingreadiness and a resolved links.self. Delivery and thumbnail URLs use only opaque media.wahlu.com asset identifiers.

Default calls keep the existing response with items and has_more. Add pagination=cursor to receive next_cursor. Pass that value ascursor with the same brand and pagination setting until it is null. An empty page with a cursor still advances the scan. Cursors cannot be reused for another brand.

Query parameters

ParameterTypeDescription
limitintegerItems to return, from 1 to 24. Defaults to 12.
paginationstringSet to cursor to opt in to paginated browsing.
cursorstringThe next_cursor from the previous page. Requires pagination=cursor.
curl
curl "https://api.wahlu.com/v1/brands/brand_01k0acme/media?limit=12" \
  -H "Authorization: Bearer $WAHLU_API_KEY"
200 response
{
  "success": true,
  "data": {
    "items": [
      {
        "id": "media_01k0hero",
        "file_name": "hero.jpg",
        "content_type": "image/jpeg",
        "size": 245760,
        "duration": null,
        "status": "completed",
        "label_ids": [],
        "folder_id": null,
        "download_url": "https://media.wahlu.com/assets/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
        "thumbnail_large_url": null,
        "thumbnail_small_url": null,
        "source": "remote_import",
        "description": null,
        "last_used_at": null,
        "created_at": "2026-07-19T02:00:00.000Z",
        "updated_at": "2026-07-19T02:00:05.000Z",
        "readiness": {
          "ready_for_content": true,
          "next_actions": []
        },
        "links": {
          "self": {
            "href": "/v1/brands/brand_01k0acme/media/media_01k0hero",
            "method": "GET",
            "required_scopes": ["media:read"]
          }
        }
      }
    ],
    "has_more": false
  },
  "meta": { "request_id": "req_media_list_01k0" }
}

Get a Media item

GET/v1/brands/:brand_id/media/:media_id

Requires media:read. Followlinks.self.href from the import result. One explicit read returns the current resource, structured readiness, a boundednext_actions array, and the same resolved self link.

Attach the ID to content only whenreadiness.ready_for_content is true. When it is false, surface the returned guidance: transient processing returnsread_media_again, while a failed or unavailable asset returnsrepair_media. The API does not hide a polling loop inside this read operation.

One explicit readiness read
const mediaUrl = "https://api.wahlu.com" + imported.links.self.href;
const mediaRes = await fetch(
  mediaUrl,
  { headers: { Authorization: "Bearer wahlu_live_your_api_key_here" } }
);
const mediaPayload = await mediaRes.json();
if (!mediaRes.ok) {
  throw new Error(mediaPayload.error?.message ?? "Could not read media");
}

const media = mediaPayload.data;
if (!media.readiness.ready_for_content) {
  const guidance = media.readiness.next_actions
    .map((action) => action.guidance)
    .join(" ");
  throw new Error(guidance || "Media is not ready for content");
}

const mediaId = media.id;
200 ready response
{
  "success": true,
  "data": {
    "id": "media_01k0hero",
    "file_name": "hero.jpg",
    "content_type": "image/jpeg",
    "size": 245760,
    "duration": null,
    "status": "completed",
    "label_ids": [],
    "folder_id": null,
    "download_url": "https://media.wahlu.com/assets/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
    "thumbnail_large_url": null,
    "thumbnail_small_url": null,
    "source": "remote_import",
    "description": null,
    "last_used_at": null,
    "created_at": "2026-07-19T02:00:00.000Z",
    "updated_at": "2026-07-19T02:00:05.000Z",
    "readiness": {
      "ready_for_content": true,
      "next_actions": []
    },
    "links": {
      "self": {
        "href": "/v1/brands/brand_01k0acme/media/media_01k0hero",
        "method": "GET",
        "required_scopes": ["media:read"]
      }
    }
  },
  "meta": { "request_id": "req_media_ready_01k0" }
}