Посты и расписание

Пост — это подпись, медиа, список каналов и время публикации. castbase проверяет его по правилам Instagram, ставит в расписание и публикует в назначенное время.

Создание поста

POST /v1/posts (обязателен Idempotency-Key):

Поле Описание
channel_ids список id каналов проекта (обязательно); у каждого канала своя цель публикации
media список {media_id, role?} (обязательно); role — item (по умолчанию) или cover (обложка Reels)
caption подпись, до 2200 символов и 30 хэштегов
publish_at когда публиковать (см. ниже)
external_ref ваш идентификатор поста до 128 символов, уникальный в проекте: по нему легко сопоставить пост с вашей системой
options share_to_feed (boolean), audio_name (строка до 200 символов) — для Reels; dry_run (boolean) — теневой прогон, см. ниже
# Создание поста. Повтор с тем же Idempotency-Key не создаст второй пост
PUBLISH_AT="${CASTBASE_PUBLISH_AT:-$(date -u -v+10M +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -d '+10 minutes' +%Y-%m-%dT%H:%M:%SZ)}"
POST_ID=$(curl -sS --fail-with-body -X POST "$API_URL/v1/posts" \
  -H "$AUTH" -H "Idempotency-Key: $(uuidgen)" -H "Content-Type: application/json" \
  -d "$(jq -n --arg channel "$CHANNEL_ID" --arg media "$MEDIA_ID" --arg at "$PUBLISH_AT" \
    '{channel_ids: [$channel], caption: "Привет из castbase! #api", media: [{media_id: $media}], publish_at: $at}')" | jq -r '.id')
echo "post_id: $POST_ID"

Вид поста определяется по медиа: одна картинка — фото, 2–20 картинок — карусель, одно видео — Reels (подробнее — Особенности Instagram).

Ответ 201 — пост со статусом scheduled и целью (targets[]) на каждый канал:

{
  "id": "01a1ab14-b9ae-7504-b304-8e6602f58395",
  "status": "scheduled",
  "kind": "image",
  "external_ref": null,
  "caption": "Привет из castbase! #api",
  "publish_at": "2026-11-05T09:10:00+00:00",
  "expires_at": "2026-11-06T09:10:00+00:00",
  "options": {},
  "media": [{"media_id": "01a1ab14-96b9-74f1-bd17-9602e8f39b02", "role": "item"}],
  "targets": [{
    "id": "01a1ab14-b9af-7ab3-8e2b-4bb0ff2a1c03",
    "channel_id": "01a1ab14-9628-798c-b1d0-77dbd5211c06",
    "status": "scheduled",
    "external_id": null,
    "permalink": null,
    "retry_at": null,
    "error": null,
    "attempts": 0
  }],
  "created_at": "2026-11-05T09:00:00+00:00"
}

Все времена в ответах — в UTC.

Теневой прогон: options.dry_run

"options": {"dry_run": true} проводит пост через всю цепочку Instagram до готового контейнера, но без публикации: медиа загружаются и обрабатываются площадкой, а сам пост не выходит. Так можно проверить креатив и подключение «вживую» и ничего не опубликовать.

  • Успешный прогон завершается статусом dry_run_ok (у цели и у поста); в ленте — событие post.dry_run_ok.
  • В ответе поста есть поле dry_run: true, а у цели — container_id (контейнер Instagram, он же приходит в data событий post.*).
  • Теневой пост не тратит лимит тарифа и навсегда держит свой external_ref: для теневых прогонов используйте отдельный external_ref (например, с суффиксом :dry), иначе настоящий пост с тем же ref получит 409 external_ref_taken.
  • Отличие от POST /v1/posts:validate: validate проверяет только правила и ничего не отправляет в Instagram, а dry_run проходит реальную обработку медиа площадкой.

Проверка без постановки

POST /v1/posts:validate принимает то же тело и проверяет его, ничего не создавая: 200 с видом поста и временем публикации либо 422 с errors[]. Удобно проверять креатив, пока вы его собираете.

Время публикации: publish_at

Значение Что произойдёт
ISO-8601 с зоной: 2026-12-01T10:00:00+03:00 пост выйдет в это время
ISO-8601 без зоны: 2026-12-01T10:00:00 время читается в часовом поясе проекта
не передано, пусто, время в прошлом публикация в ближайший тик (почти сразу)
"next_slot" ближайший свободный слот сетки проекта (см. ниже)
дальше 75 дней вперёд 422 too_far

Instagram принимает публикацию с небольшим опозданием, но сервис старается уложиться в две минуты после publish_at (без учёта времени, которое Instagram тратит на обработку видео). Если публикацию не удалось выполнить в течение 24 часов от publish_at, цель завершается ошибкой expired.

Сетка расписания

Сетка — недельные слоты проекта: день недели (1 — понедельник … 7 — воскресенье) и время HH:MM в часовом поясе проекта. Задать её можно в кабинете («Расписание») или через API:

curl -X PUT https://castbase.ru/v1/schedule \
  -H "Authorization: Bearer $CASTBASE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"slots": [{"weekday": 1, "time": "09:00"}, {"weekday": 3, "time": "18:30"}, {"weekday": 5, "time": "12:00"}]}'

GET /v1/schedule возвращает timezone и slots[]. Пост с "publish_at": "next_slot" занимает ближайший слот, который свободен сразу во всех выбранных каналах; следующий пост займёт следующий слот. Если сетки нет, next_slot вернёт 422 no_schedule: либо задайте сетку, либо указывайте время явно.

Список, отмена, повтор

  • GET /v1/posts?status=scheduled&limit=50&cursor=… — посты проекта, новые сверху; ответ {data[], next_cursor} (next_cursor — значение для следующей страницы, null — конец). Статусы: scheduled, processing, published, partially_published, failed, cancelled.
  • GET /v1/posts/{id} — один пост со статусом по каждой цели.
  • DELETE /v1/posts/{id} — отмена: 204, пока все цели в статусе scheduled; после отправки в публикацию — 409 post_not_cancellable. external_ref отменённого поста освобождается.
  • POST /v1/post-targets/{id}:retry — повторить цель в статусе failed (не outcome_unknown: см. раздел Статусы и события).

Посты нескольких каналов

В channel_ids можно передать несколько каналов: у поста одна подпись и одни медиа, а целей столько же, сколько каналов. Результат по каждой цели независим: пост получит статус partially_published, если часть целей опубликована, а часть — нет.