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
/v1/brands/:brand_id/content-itemsRequires 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
| Parameter | Type | Description |
|---|---|---|
name* | string | Trimmed draft name, up to 500 characters. |
idempotency_key | string | Body alternative to Idempotency-Key. If both are sent, they must match. |
copy_mode | string | single or per_platform. |
single_copy | object | null | Shared caption, hashtags, and optional title when copy_mode is single. |
platform_copy | object | null | Per-platform caption objects when copy_mode is per_platform. |
*_settings | object | null | Platform media and publishing options. Supported keys are returned by platform capabilities. |
intended_integration_ids | string[] | Up to 20 unique, canonically sorted target IDs for the next preflight. |
label_ids | string[] | Optional unique label IDs in canonical ascending order. |
thumbnail_media_id | string | Optional Media ID for the draft thumbnail. |
thumbnail_timestamp | number | Optional 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.
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"]
}'{
"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
/v1/brands/:brand_id/content-items/:content_item_id/tiktok-privacyRequires 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.
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"
}'{
"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
/v1/brands/:brand_id/content-items/:content_item_id/preflightRequires 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
| Parameter | Type | Description |
|---|---|---|
integration_ids* | string[] | One to 20 unique integration IDs in canonical order. |
scheduled_at | string | ISO 8601 date and time with an explicit timezone offset or Z suffix. |
approval_status | string | approved, pending_review, or rejected. Send pending_review for a held workflow. |
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"
}'{
"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.