Schedules
Create, list and read Schedules, and reschedule or cancel one before its publish run starts.
For draft edits, deletion and held-schedule review, see the drafts, History and planning reference.
A Schedule is a durable publishing instruction, not an execution attempt. The released surface creates a Schedule, lists and reads Schedules, and reschedules or cancels one before its publish run starts. 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 (the Publishing permission) because it lets the post go out to your connected social media accounts.
Request body
| Parameter | Type | Description |
|---|---|---|
content_item_id (required) | string | The draft ID returned by content creation. |
scheduled_at (required) | string | ISO 8601 date and time with an explicit timezone offset or Z suffix. |
integration_ids (required) | string[] | One to 20 unique target integration IDs. |
approval_status (required) | 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");
}List Schedules
/v1/brands/:brand_id/schedulesRequires schedule:read. Returns one bounded page of the brand's Schedules whose scheduled time falls in an explicit window, ordered by scheduled time. Each item has the same shape as a single Schedule read, with its approval, status, blocker, latest execution summary, and next action. Like the single read, it changes nothing.
from is inclusive and to is exclusive. Both are required, must be UTC timestamps with millisecond precision, and can be at most 93 days apart. Whenhas_more is true, send next_cursor as cursor with the same window and label filter to read the next page.
Query parameters
| Parameter | Type | Description |
|---|---|---|
from (required) | string | Window start, for example 2026-07-01T00:00:00.000Z. Inclusive. |
to (required) | string | Window end, after from and at most 93 days later. Exclusive. |
limit | integer | Items to return, from 1 to 50. Defaults to 31. |
cursor | string | The next_cursor from the previous page. |
label | string | Optional exact label ID. Returns schedules whose content has this label in this brand. |
order_direction | string | asc (earliest first, the default) or desc. |
curl "https://api.wahlu.com/v1/brands/brand_01k0acme/schedules?from=2026-07-01T00:00:00.000Z&to=2026-08-01T00:00:00.000Z&limit=31" \
-H "Authorization: Bearer wahlu_live_your_api_key_here"{
"success": true,
"data": {
"items": [
{
"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"]
}
}
}
],
"has_more": false,
"next_cursor": null
},
"meta": { "request_id": "req_schedule_list_01k0" }
}Reschedule a Schedule
/v1/brands/:brand_id/schedules/:schedule_id/rescheduleRequires schedule:write and publish:execute, plus a caller-owned idempotency key. Send the new time and repeat the Schedule ID inconfirm_schedule_id; a mismatch is rejected with422 DOMAIN_VALIDATION_FAILED. Only the time changes: approval, targets and content stay as they are.
A Schedule can be moved only before its publish run starts. Once a run is processing or has run, the request returns 409 with CONFLICT. The command changes the stored instruction only: it calls no provider and does not claim anything was published.
Request body
| Parameter | Type | Description |
|---|---|---|
scheduled_at (required) | string | ISO 8601 date and time with an explicit timezone offset or Z suffix. |
confirm_schedule_id (required) | string | Must exactly match the schedule_id in the path. |
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/schedule_01k0launch/reschedule \
-H "Authorization: Bearer wahlu_live_your_api_key_here" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: reschedule-autumn-launch-v1" \
-d '{
"scheduled_at": "2026-07-21T10:30:00+10:00",
"confirm_schedule_id": "schedule_01k0launch"
}'{
"success": true,
"data": {
"schedule": {
"id": "schedule_01k0launch",
"brand_id": "brand_01k0acme",
"content_item_id": "content_01k0launch",
"integration_ids": ["integration_01k0instagram"],
"scheduled_at": "2026-07-21T00:30:00.000Z",
"approval_status": "approved",
"status": "scheduled",
"blocking_reason": null,
"latest_execution": null,
"next_actions": [
{
"action": "wait_for_execution",
"guidance": "Wait for the scheduled time, then read this schedule again."
}
],
"created_at": "2026-07-19T02:00:00.000Z",
"updated_at": "2026-07-19T03: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": "rescheduled"
},
"meta": { "request_id": "req_schedule_rescheduled_01k0" }
}Cancel a Schedule
/v1/brands/:brand_id/schedules/:schedule_id/cancelRequires schedule:write and a caller-owned idempotency key. Repeat the Schedule ID in confirm_schedule_id. Cancelling works only while no publish run has started; otherwise it returns 409 with CONFLICT.
The Schedule is kept for audit with status: "cancelled" and no next action. Cancelling calls no provider and never deletes content from a platform.
Request body
| Parameter | Type | Description |
|---|---|---|
confirm_schedule_id (required) | string | Must exactly match the schedule_id in the path. |
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/schedule_01k0launch/cancel \
-H "Authorization: Bearer wahlu_live_your_api_key_here" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: cancel-autumn-launch-v1" \
-d '{ "confirm_schedule_id": "schedule_01k0launch" }'{
"success": true,
"data": {
"schedule": {
"id": "schedule_01k0launch",
"brand_id": "brand_01k0acme",
"content_item_id": "content_01k0launch",
"integration_ids": ["integration_01k0instagram"],
"scheduled_at": "2026-07-21T00:30:00.000Z",
"approval_status": "approved",
"status": "cancelled",
"blocking_reason": null,
"latest_execution": null,
"next_actions": [],
"created_at": "2026-07-19T02:00:00.000Z",
"updated_at": "2026-07-19T03:05: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": "cancelled"
},
"meta": { "request_id": "req_schedule_cancelled_01k0" }
}For both commands, an exact idempotent replay returns the same Schedule with200, outcome: "replayed", andIdempotency-Replayed: true. Reusing the key with different input returns 409 with IDEMPOTENCY_KEY_CONFLICT.
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.