Посты и расписание
Пост — это подпись, медиа, список каналов и время публикации. 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"
# Создание поста. Повтор с тем же Idempotency-Key не создаст второй пост
publish_at = datetime.now(timezone.utc) + timedelta(minutes=10)
post = api(
"POST", "/v1/posts",
body={
"channel_ids": [channel_id],
"caption": "Привет из castbase! #api",
"media": [{"media_id": media["media_id"]}],
"publish_at": os.environ.get("CASTBASE_PUBLISH_AT") or publish_at.isoformat(timespec="seconds"),
},
idempotency_key=str(uuid.uuid4()),
)
print("post_id:", post["id"], "status:", post["status"])
// Создание поста. Повтор с тем же Idempotency-Key не создаст второй пост
const publishAt = new Date(Date.now() + 10 * 60 * 1000).toISOString();
const post = await api("POST", "/v1/posts", {
json: {
channel_ids: [channelId],
caption: "Привет из castbase! #api",
media: [{ media_id: media.media_id }],
publish_at: process.env.CASTBASE_PUBLISH_AT || publishAt,
},
idempotencyKey: randomUUID(),
});
console.log("post_id:", post.id, "status:", post.status);
// Создание поста. Повтор с тем же Idempotency-Key не создаст второй пост
$post = api('POST', '/v1/posts', [
'channel_ids' => [$channelId],
'caption' => 'Привет из castbase! #api',
'media' => [['media_id' => $media['media_id']]],
'publish_at' => getenv('CASTBASE_PUBLISH_AT') ?: (new DateTimeImmutable('+10 minutes', new DateTimeZone('UTC')))->format(DATE_ATOM),
], idempotencyKey: uuid4());
echo 'post_id: ', $post['id'], ' status: ', $post['status'], "\n";
// Создание поста. Повтор с тем же Idempotency-Key не создаст второй пост
publishAt := envOr("CASTBASE_PUBLISH_AT", time.Now().UTC().Add(10*time.Minute).Format(time.RFC3339))
var post struct {
ID string `json:"id"`
Status string `json:"status"`
Targets []target `json:"targets"`
}
postJSON("/v1/posts", map[string]any{
"channel_ids": []string{channelID},
"caption": "Привет из castbase! #api",
"media": []map[string]string{{"media_id": media.MediaID}},
"publish_at": publishAt,
}, &post)
fmt.Println("post_id:", post.ID, "status:", post.Status)
Вид поста определяется по медиа: одна картинка — фото, 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, если часть целей опубликована, а часть — нет.