Wahlu API

Content Items

Create an attached content draft and check its scheduling readiness.

The released Content API creates an idempotent draft and runs a separate, write-free preflight. Creating a draft never creates a Schedule or Job, calls a provider, or publishes content.

Captions use copy_mode with single_copy orplatform_copy. Platform settings hold media and publishing options only. Discover current platform rules from the capabilities operation before constructing settings.

Create a content draft

POST/v1/brands/:brand_id/content-items

Requires posts:write. Attach only a Media ID whosereadiness.ready_for_content is true. See the Media reference.

Send either an Idempotency-Key header or anidempotency_key body field. If both are sent, they must match. A new draft returns 201; an exact replay returns the saved draft with 200 and Idempotency-Replayed: true. Reusing a key with different input returns 409.

Request body

ParameterTypeDescription
name*stringTrimmed draft name, up to 500 characters.
idempotency_keystringBody alternative to Idempotency-Key. If both are sent, they must match.
copy_modestringsingle or per_platform.
single_copyobject | nullShared caption, hashtags, and optional title when copy_mode is single.
platform_copyobject | nullPer-platform caption objects when copy_mode is per_platform.
*_settingsobject | nullPlatform media and publishing options. Supported keys are returned by platform capabilities.
intended_integration_idsstring[]Up to 20 unique, canonically sorted target IDs for the next preflight.
label_idsstring[]Optional unique label IDs in canonical ascending order.
thumbnail_media_idstringOptional Media ID for the draft thumbnail.
thumbnail_timestampnumberOptional non-negative video thumbnail time in seconds.

The body is strict snake_case. Unknown keys, camelCase aliases, padded identifiers, duplicate IDs, and non-canonical sorted arrays are rejected instead of silently changed.

Create an attached Instagram draft
curl -X POST https://api.wahlu.com/v1/brands/brand_01k0acme/content-items \
  -H "Authorization: Bearer wahlu_live_your_api_key_here" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: draft-autumn-launch-v1" \
  -d '{
    "name": "Autumn launch announcement",
    "copy_mode": "single",
    "single_copy": {
      "caption": "Our autumn collection is ready.",
      "hashtags": ["autumn", "launch"],
      "title": "Autumn launch"
    },
    "instagram_settings": {
      "media_ids": ["media_01k0hero"],
      "post_type": "REEL",
      "trial_reel": false,
      "collaborators": []
    },
    "intended_integration_ids": ["integration_01k0instagram"]
  }'
201 response
{
  "success": true,
  "data": {
    "content_item": {
      "id": "content_01k0launch",
      "brand_id": "brand_01k0acme",
      "name": "Autumn launch announcement",
      "label_ids": [],
      "thumbnail_media_id": null,
      "thumbnail_timestamp": 0,
      "intended_integration_ids": ["integration_01k0instagram"],
      "copy_mode": "single",
      "single_copy": {
        "caption": "Our autumn collection is ready.",
        "hashtags": ["autumn", "launch"],
        "title": "Autumn launch"
      },
      "platform_copy": null,
      "instagram_settings": {
        "media_ids": ["media_01k0hero"],
        "post_type": "REEL",
        "trial_reel": false,
        "graduation_strategy": null,
        "collaborators": []
      },
      "tiktok_settings": null,
      "facebook_settings": null,
      "youtube_settings": null,
      "linkedin_settings": null,
      "google_business_settings": null,
      "created_at": "2026-07-18T02:00:00.000Z",
      "updated_at": "2026-07-18T02:00:00.000Z"
    },
    "outcome": "created",
    "intended_integration_ids": ["integration_01k0instagram"],
    "next_actions": [
      {
        "action": "preflight_content",
        "integration_ids": ["integration_01k0instagram"]
      }
    ],
    "links": {
      "targets": {
        "href": "/v1/brands/brand_01k0acme/targets",
        "required_scopes": ["integrations:read"]
      },
      "preflight": {
        "href": "/v1/brands/brand_01k0acme/content-items/content_01k0launch/preflight",
        "method": "POST",
        "required_scopes": ["schedule:write"]
      }
    }
  },
  "meta": { "request_id": "req_draft_created_01k0" }
}

Update TikTok privacy on the existing draft

PUT/v1/brands/:brand_id/content-items/:content_item_id/tiktok-privacy

Requires posts:write. Send the exact TikTok integration ID selected for this draft and one privacy value returned by its live dynamic-options operation. Wahlu performs a fresh target-specific check immediately before the write, so a stale or unsupported value fails clearly and the draft stays unchanged. Wahlu never guesses PUBLIC_TO_EVERYONE.

This operation changes only tiktok_settings.privacy_level on the same content item. It creates no replacement draft, Schedule, Job, or provider post. Repeating the same supported value returns an unchanged outcome. It is naturally idempotent and does not accept an Idempotency-Key header. Drafts already controlled by active or approved publication intent are rejected.

Set a validated TikTok privacy choice
curl -X PUT https://api.wahlu.com/v1/brands/brand_01k0acme/content-items/content_01k0launch/tiktok-privacy \
  -H "Authorization: Bearer $WAHLU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "integration_id": "integration_01k0tiktok",
    "privacy_level": "SELF_ONLY"
  }'
200 response
{
  "success": true,
  "data": {
    "content_item_id": "content_01k0launch",
    "brand_id": "brand_01k0acme",
    "integration_id": "integration_01k0tiktok",
    "privacy_level": "SELF_ONLY",
    "outcome": "updated",
    "next_action": { "action": "preflight_content" },
    "links": {
      "self": {
        "href": "/v1/brands/brand_01k0acme/content-items/content_01k0launch/tiktok-privacy",
        "method": "PUT",
        "required_scopes": ["posts:write"]
      },
      "preflight": {
        "href": "/v1/brands/brand_01k0acme/content-items/content_01k0launch/preflight",
        "method": "POST",
        "required_scopes": ["schedule:write"]
      },
      "dynamic_options": {
        "href": "/v1/brands/brand_01k0acme/targets/integration_01k0tiktok/dynamic-options",
        "required_scopes": ["integrations:read"]
      }
    }
  },
  "meta": { "request_id": "req_tiktok_privacy_01k0" }
}

Preflight a content draft

POST/v1/brands/:brand_id/content-items/:content_item_id/preflight

Requires schedule:write. This is a write-free readiness check for the returned draft ID and selected integration IDs. It creates no Schedule or Job, enqueues nothing, calls no provider, and publishes nothing.

Request body

ParameterTypeDescription
integration_ids*string[]One to 20 unique integration IDs in canonical order.
scheduled_atstringISO 8601 date and time with an explicit timezone offset or Z suffix.
approval_statusstringapproved, pending_review, or rejected. Send pending_review for a held workflow.
Preflight for a held Schedule
curl -X POST https://api.wahlu.com/v1/brands/brand_01k0acme/content-items/content_01k0launch/preflight \
  -H "Authorization: Bearer wahlu_live_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "integration_ids": ["integration_01k0instagram"],
    "scheduled_at": "2026-07-20T12:00:00+10:00",
    "approval_status": "pending_review"
  }'
200 ready response
{
  "success": true,
  "data": {
					"validation_version": "2026-08-05.1",
    "fingerprint": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
    "content_item": {
      "id": "content_01k0launch",
      "brand_id": "brand_01k0acme",
      "updated_at": "2026-07-18T02:00:00.000Z"
    },
    "request": {
      "integration_ids": ["integration_01k0instagram"],
      "scheduled_at": "2026-07-20T02:00:00.000Z",
      "approval_status": "pending_review"
    },
    "policy": { "scheduling_allowed": true },
    "can_schedule": true,
    "target_count": 1,
    "targets": [
      {
        "integration_id": "integration_01k0instagram",
        "platform": "instagram",
        "display_name": "Acme Instagram",
        "connection_status": "connected",
        "can_schedule": true,
        "issues": [],
        "blockers": [],
        "warnings": []
      }
    ],
    "issues": [
      {
        "severity": "warning",
        "code": "APPROVAL_PENDING",
        "scope": "approval",
        "field_path": "$.approval_status",
        "message": "The schedule can be created, but publishing remains gated until approval.",
        "integration_id": null,
        "platform": null,
        "repair_action": {
          "id": "review_content",
          "action": "change_approval",
          "label": "Review the content",
          "guidance": "Approve the content before its execution time."
        }
      }
    ],
    "blockers": [],
    "warnings": [
      {
        "severity": "warning",
        "code": "APPROVAL_PENDING",
        "scope": "approval",
        "field_path": "$.approval_status",
        "message": "The schedule can be created, but publishing remains gated until approval.",
        "integration_id": null,
        "platform": null,
        "repair_action": {
          "id": "review_content",
          "action": "change_approval",
          "label": "Review the content",
          "guidance": "Approve the content before its execution time."
        }
      }
    ],
    "next_actions": [
      "Create the schedule; the mutation will re-run this preflight before writing."
    ],
    "links": {
      "self": {
        "href": "/v1/brands/brand_01k0acme/content-items/content_01k0launch/preflight",
        "method": "POST"
      },
      "targets": {
        "href": "/v1/brands/brand_01k0acme/targets",
        "required_scopes": ["integrations:read"]
      },
      "create_schedule": {
        "href": "/v1/brands/brand_01k0acme/schedules",
        "method": "POST",
        "required_scopes": ["schedule:write"]
      }
    }
  },
  "meta": { "request_id": "req_preflight_ready_01k0" }
}

Decide from the response

A successful HTTP response can still have can_schedule: false. When blocked, apply the returned repair guidance and explicitly run preflight again. links.create_schedule appears only whencan_schedule is true.

Supported dimension or aspect-ratio blockers include deterministic repair_optionsfor the exact integration, platform, post type, source hash, policy, safe area, and transform geometry. Choose centre_crop or pad explicitly and pass that option to the Media repair-derivative endpoint. If no safe transform is available, preflight returns a machine-readable unavailable code and explanation instead of repairing implicitly.

The link's authority matches the requested approval: a pending_review Schedule requires onlyschedule:write, while approved additionally requires publish:execute. Follow that link into the Schedule reference.