Skip to content
Wahlu API

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

POST/v1/brands/:brand_id/schedules

Requires 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

ParameterTypeDescription
content_item_id (required)stringThe draft ID returned by content creation.
scheduled_at (required)stringISO 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)stringapproved, pending_review, or rejected. Never inferred.
idempotency_keystringBody alternative to Idempotency-Key. If both are sent, they must match.
Create a held Schedule
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"
  }'
201 held response
{
  "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

GET/v1/brands/:brand_id/schedules/:schedule_id

Requires 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.

One explicit read
curl https://api.wahlu.com/v1/brands/brand_01k0acme/schedules/schedule_01k0launch \
  -H "Authorization: Bearer wahlu_live_your_api_key_here"
200 held response
{
  "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" }
}
Check the held state
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

GET/v1/brands/:brand_id/schedules

Requires 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

ParameterTypeDescription
from (required)stringWindow start, for example 2026-07-01T00:00:00.000Z. Inclusive.
to (required)stringWindow end, after from and at most 93 days later. Exclusive.
limitintegerItems to return, from 1 to 50. Defaults to 31.
cursorstringThe next_cursor from the previous page.
labelstringOptional exact label ID. Returns schedules whose content has this label in this brand.
order_directionstringasc (earliest first, the default) or desc.
List one month
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"
200 response
{
  "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

POST/v1/brands/:brand_id/schedules/:schedule_id/reschedule

Requires 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

ParameterTypeDescription
scheduled_at (required)stringISO 8601 date and time with an explicit timezone offset or Z suffix.
confirm_schedule_id (required)stringMust exactly match the schedule_id in the path.
idempotency_keystringBody alternative to Idempotency-Key. If both are sent, they must match.
Move a Schedule
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"
  }'
200 response
{
  "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

POST/v1/brands/:brand_id/schedules/:schedule_id/cancel

Requires 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

ParameterTypeDescription
confirm_schedule_id (required)stringMust exactly match the schedule_id in the path.
idempotency_keystringBody alternative to Idempotency-Key. If both are sent, they must match.
Cancel before the run starts
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" }'
200 response
{
  "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

GET/v1/brands/:brand_id/schedules/:schedule_id/receipt

Requires 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

POST/v1/brands/:brand_id/schedules/:schedule_id/provider-cleanup

Requires 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.