Schedules
Create an explicitly approved or held Schedule and read its current safe status.
A Schedule is a durable publishing instruction, not an execution attempt. The released surface creates one Schedule and reads that same resource. Run the write-free preflight first, then follow its resolved links.create_schedule only whencan_schedule is true.
Create a Schedule
/v1/brands/:brand_id/schedulesRequires schedule:write and a caller-owned idempotency key. The approval decision is always explicit. Apending_review Schedule is safely held; anapproved request additionally requirespublish:execute because it authorises external publishing.
Request body
| Parameter | Type | Description |
|---|---|---|
content_item_id* | string | The draft ID returned by content creation. |
scheduled_at* | string | ISO 8601 date and time with an explicit timezone offset or Z suffix. |
integration_ids* | string[] | One to 20 unique target integration IDs. |
approval_status* | string | approved, pending_review, or rejected. Never inferred. |
idempotency_key | string | Body alternative to Idempotency-Key. If both are sent, they must match. |
curl -X POST https://api.wahlu.com/v1/brands/brand_01k0acme/schedules \
-H "Authorization: Bearer wahlu_live_your_api_key_here" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: schedule-autumn-launch-v1" \
-d '{
"content_item_id": "content_01k0launch",
"scheduled_at": "2026-07-20T12:00:00+10:00",
"integration_ids": ["integration_01k0instagram"],
"approval_status": "pending_review"
}'{
"success": true,
"data": {
"schedule": {
"id": "schedule_01k0launch",
"brand_id": "brand_01k0acme",
"content_item_id": "content_01k0launch",
"integration_ids": ["integration_01k0instagram"],
"scheduled_at": "2026-07-20T02:00:00.000Z",
"approval_status": "pending_review",
"status": "action_required",
"blocking_reason": {
"code": "APPROVAL_PENDING",
"message": "This schedule is waiting for approval.",
"repair_guidance": "Approve the content before its scheduled time."
},
"latest_execution": null,
"next_actions": [
{
"action": "review_approval",
"guidance": "Approve the content before its scheduled time."
}
],
"created_at": "2026-07-19T02:00:00.000Z",
"updated_at": "2026-07-19T02:00:00.000Z",
"links": {
"self": {
"href": "/v1/brands/brand_01k0acme/schedules/schedule_01k0launch",
"required_scopes": ["schedule:read"]
},
"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"]
}
}
},
"outcome": "created"
},
"meta": { "request_id": "req_schedule_created_01k0" }
}A new Schedule returns 201 withoutcome: "created". An exact idempotent replay returns the same Schedule with 200,outcome: "replayed", andIdempotency-Replayed: true. Reusing the key with different input returns 409.
The held result is explicit:status: "action_required", blockerAPPROVAL_PENDING, and latest_execution: null. It creates no execution or Job, sends no provider request, and publishes nothing.
Get a Schedule
/v1/brands/:brand_id/schedules/:schedule_idRequires schedule:read. Follow the created Schedule'slinks.self.href for one bounded read of its approval, status, blocker, latest execution summary (including the opaque latest_execution.id when a run exists), and next action. The run ID is only a Wahlu correlation handle; it is not a provider ID. The read does not modify the Schedule, create an execution, or start polling.
curl https://api.wahlu.com/v1/brands/brand_01k0acme/schedules/schedule_01k0launch \
-H "Authorization: Bearer wahlu_live_your_api_key_here"{
"success": true,
"data": {
"id": "schedule_01k0launch",
"brand_id": "brand_01k0acme",
"content_item_id": "content_01k0launch",
"integration_ids": ["integration_01k0instagram"],
"scheduled_at": "2026-07-20T02:00:00.000Z",
"approval_status": "pending_review",
"status": "action_required",
"blocking_reason": {
"code": "APPROVAL_PENDING",
"message": "This schedule is waiting for approval.",
"repair_guidance": "Approve the content before its scheduled time."
},
"latest_execution": null,
"next_actions": [
{
"action": "review_approval",
"guidance": "Approve the content before its scheduled time."
}
],
"created_at": "2026-07-19T02:00:00.000Z",
"updated_at": "2026-07-19T02:00:00.000Z",
"links": {
"self": {
"href": "/v1/brands/brand_01k0acme/schedules/schedule_01k0launch",
"required_scopes": ["schedule:read"]
},
"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_schedule_read_01k0" }
}const schedule = payload.data;
if (
schedule.approval_status !== "pending_review" ||
schedule.status !== "action_required" ||
schedule.blocking_reason?.code !== "APPROVAL_PENDING" ||
schedule.latest_execution !== null
) {
throw new Error("Schedule is not in the expected held state");
}Get a publish receipt
/v1/brands/:brand_id/schedules/:schedule_id/receiptRequires schedule:read. This bounded read returns the exact latest publish run with redacted per-platform outcomes. It never returns provider IDs, provider errors, tokens, payloads, or arbitrary prior runs. A cleanup authority and link appear only when that exact run is safely bounded.
Queue exact provider cleanup
/v1/brands/:brand_id/schedules/:schedule_id/provider-cleanupRequires publish:execute, a caller-owned idempotency key, and thecleanup_authority returned by the matching publish receipt. The request accepts no provider IDs, cannot search or replay arbitrary history, and queues cleanup only for eligible published records covered by that exact authority. No provider is called by this request; failed and action-required targets are skipped.