Быстрый старт
За пять минут вы пройдёте путь от регистрации до поста, поставленного в расписание. Весь код ниже — рабочие примеры из
репозитория castbase-examples; выберите свой язык на любой вкладке, выбор запомнится для всей документации.
1. Регистрация
Откройте регистрацию, введите email и пароль, отметьте два согласия (на обработку персональных данных и с пользовательским соглашением) и подтвердите почту по ссылке из письма.
2. Проект
После входа нажмите «Создать проект». Проект — рабочее пространство: у него своё расписание, свои каналы, свои API-ключи
и свои лимиты. Укажите название и часовой пояс (по умолчанию Europe/Moscow): в этом поясе читаются слоты расписания и
время publish_at без указания зоны.
3. Подключите Instagram
В проекте откройте «Каналы» → «Подключить Instagram». Мастер за пять шагов проведёт вас по настройкам Meta и в конце
попросит вставить токен. Канал сначала в статусе pending («проверяем…»), через минуту — active с именем аккаунта.
Подробнее — в разделе Подключение Instagram.
4. API-ключ
«API-ключи» → «Выпустить ключ». Ключ вида cb_live_… показывается один раз, скопируйте его. Передайте его в переменную
окружения — все примеры читают ключ оттуда:
export CASTBASE_API_KEY=cb_live_...
export CASTBASE_API_URL=https://castbase.ru # необязательно, это значение по умолчанию
Каждый пример начинается с одной и той же настройки:
API_URL="${CASTBASE_API_URL:-https://castbase.ru}"
API_KEY="${CASTBASE_API_KEY:?Задайте CASTBASE_API_KEY (ключ проекта: кабинет -> API-ключи)}"
AUTH="Authorization: Bearer ${API_KEY}"
API_URL = os.environ.get("CASTBASE_API_URL", "https://castbase.ru").rstrip("/")
API_KEY = os.environ.get("CASTBASE_API_KEY")
if not API_KEY:
sys.exit("Задайте CASTBASE_API_KEY (ключ проекта: кабинет -> API-ключи)")
def api(method, path, *, body=None, data=None, content_type=None, idempotency_key=None):
"""Один запрос к API. body — JSON-объект, data — готовые байты (multipart)."""
headers = {"Authorization": f"Bearer {API_KEY}", "Accept": "application/json"}
if body is not None:
data = json.dumps(body).encode()
content_type = "application/json"
if content_type:
headers["Content-Type"] = content_type
if idempotency_key:
headers["Idempotency-Key"] = idempotency_key # обязателен на каждом POST
request = urllib.request.Request(API_URL + path, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(request, timeout=120) as response:
raw = response.read()
except urllib.error.HTTPError as error:
# Ошибки приходят как application/problem+json: code, detail, errors[]
sys.exit(f"{method} {path} -> {error.code}: {error.read().decode()}")
return json.loads(raw) if raw else None
const API_URL = (process.env.CASTBASE_API_URL ?? "https://castbase.ru").replace(/\/+$/, "");
const API_KEY = process.env.CASTBASE_API_KEY;
if (!API_KEY) {
console.error("Задайте CASTBASE_API_KEY (ключ проекта: кабинет -> API-ключи)");
process.exit(1);
}
async function api(method, path, { json, form, idempotencyKey } = {}) {
const headers = { Authorization: `Bearer ${API_KEY}`, Accept: "application/json" };
let body = form;
if (json !== undefined) {
body = JSON.stringify(json);
headers["Content-Type"] = "application/json";
}
if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey; // обязателен на каждом POST
const response = await fetch(API_URL + path, { method, headers, body });
const text = await response.text();
if (!response.ok) {
// Ошибки приходят как application/problem+json: code, detail, errors[]
throw new Error(`${method} ${path} -> ${response.status}: ${text}`);
}
return text ? JSON.parse(text) : null;
}
$apiUrl = rtrim(getenv('CASTBASE_API_URL') ?: 'https://castbase.ru', '/');
$apiKey = getenv('CASTBASE_API_KEY') ?: null;
if (null === $apiKey) {
fwrite(STDERR, "Задайте CASTBASE_API_KEY (ключ проекта: кабинет -> API-ключи)\n");
exit(1);
}
function uuid4(): string
{
$b = random_bytes(16);
$b[6] = chr(ord($b[6]) & 0x0f | 0x40);
$b[8] = chr(ord($b[8]) & 0x3f | 0x80);
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($b), 4));
}
/** Один запрос к API. $body — JSON-массив или (для multipart) массив полей с CURLFile. */
function api(string $method, string $path, array|null $body = null, bool $multipart = false, ?string $idempotencyKey = null): ?array
{
global $apiUrl, $apiKey;
$headers = ["Authorization: Bearer $apiKey", 'Accept: application/json'];
if (null !== $idempotencyKey) {
$headers[] = "Idempotency-Key: $idempotencyKey"; // обязателен на каждом POST
}
$curl = curl_init($apiUrl.$path);
curl_setopt_array($curl, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 120]);
if (null !== $body) {
if ($multipart) {
curl_setopt($curl, CURLOPT_POSTFIELDS, $body);
} else {
$headers[] = 'Content-Type: application/json';
curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($body, JSON_UNESCAPED_UNICODE));
}
}
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
$text = curl_exec($curl);
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
curl_close($curl);
if (false === $text || $status >= 400) {
// Ошибки приходят как application/problem+json: code, detail, errors[]
fwrite(STDERR, "$method $path -> $status: $text\n");
exit(1);
}
return '' === $text ? null : json_decode($text, true, flags: JSON_THROW_ON_ERROR);
}
var (
apiURL = strings.TrimRight(envOr("CASTBASE_API_URL", "https://castbase.ru"), "/")
apiKey = os.Getenv("CASTBASE_API_KEY")
)
func envOr(name, fallback string) string {
if value := os.Getenv(name); value != "" {
return value
}
return fallback
}
func init() {
if apiKey == "" {
fail("Задайте CASTBASE_API_KEY (ключ проекта: кабинет -> API-ключи)")
}
}
func fail(format string, args ...any) {
fmt.Fprintf(os.Stderr, format+"\n", args...)
os.Exit(1)
}
func uuid4() string {
b := make([]byte, 16)
_, _ = rand.Read(b)
b[6] = b[6]&0x0f | 0x40
b[8] = b[8]&0x3f | 0x80
return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
}
// api делает один запрос: body — готовые байты, idempotencyKey обязателен на каждом POST.
// Ответ разбирается в out (структура или map).
func api(method, path string, body []byte, contentType, idempotencyKey string, out any) {
request, err := http.NewRequest(method, apiURL+path, bytes.NewReader(body))
if err != nil {
fail("%v", err)
}
request.Header.Set("Authorization", "Bearer "+apiKey)
request.Header.Set("Accept", "application/json")
if contentType != "" {
request.Header.Set("Content-Type", contentType)
}
if idempotencyKey != "" {
request.Header.Set("Idempotency-Key", idempotencyKey)
}
response, err := (&http.Client{Timeout: 120 * time.Second}).Do(request)
if err != nil {
fail("%s %s: %v", method, path, err)
}
defer response.Body.Close()
text, _ := io.ReadAll(response.Body)
if response.StatusCode >= 400 {
// Ошибки приходят как application/problem+json: code, detail, errors[]
fail("%s %s -> %d: %s", method, path, response.StatusCode, text)
}
if out != nil {
if err := json.Unmarshal(text, out); err != nil {
fail("%s %s: ответ не JSON: %v", method, path, err)
}
}
}
func postJSON(path string, payload any, out any) {
body, _ := json.Marshal(payload)
api("POST", path, body, "application/json", uuid4(), out)
}
5. Канал для публикации
Узнайте id канала (или передайте свой в CASTBASE_CHANNEL_ID):
# Канал, в который публикуем: свой id или первый активный
CHANNEL_ID="${CASTBASE_CHANNEL_ID:-$(curl -sf -H "$AUTH" "$API_URL/v1/channels" | jq -r '[.data[] | select(.status == "active")][0].id // empty')}"
[ -n "$CHANNEL_ID" ] || { echo "Нет активного канала: подключите Instagram в кабинете (раздел «Каналы»)" >&2; exit 1; }
# Канал, в который публикуем: свой id или первый активный
channel_id = os.environ.get("CASTBASE_CHANNEL_ID")
if not channel_id:
channels = api("GET", "/v1/channels")["data"]
active = [c for c in channels if c["status"] == "active"]
if not active:
sys.exit("Нет активного канала: подключите Instagram в кабинете (раздел «Каналы»)")
channel_id = active[0]["id"]
// Канал, в который публикуем: свой id или первый активный
let channelId = process.env.CASTBASE_CHANNEL_ID;
if (!channelId) {
const { data: channels } = await api("GET", "/v1/channels");
const active = channels.find((c) => c.status === "active");
if (!active) throw new Error("Нет активного канала: подключите Instagram в кабинете (раздел «Каналы»)");
channelId = active.id;
}
// Канал, в который публикуем: свой id или первый активный
$channelId = getenv('CASTBASE_CHANNEL_ID') ?: null;
if (null === $channelId) {
$active = array_values(array_filter(api('GET', '/v1/channels')['data'], static fn (array $c): bool => 'active' === $c['status']));
if ([] === $active) {
fwrite(STDERR, "Нет активного канала: подключите Instagram в кабинете (раздел «Каналы»)\n");
exit(1);
}
$channelId = $active[0]['id'];
}
// Канал, в который публикуем: свой id или первый активный
channelID := os.Getenv("CASTBASE_CHANNEL_ID")
if channelID == "" {
var channels struct {
Data []struct {
ID string `json:"id"`
Status string `json:"status"`
} `json:"data"`
}
api("GET", "/v1/channels", nil, "", "", &channels)
for _, c := range channels.Data {
if c.Status == "active" {
channelID = c.ID
break
}
}
if channelID == "" {
fail("Нет активного канала: подключите Instagram в кабинете (раздел «Каналы»)")
}
}
6. Загрузите медиа
Файл (JPEG, PNG или WebP до 8 МБ; видео MP4 или MOV до 300 МБ) загружается отдельным запросом и возвращает media_id.
Обратите внимание на заголовок Idempotency-Key: он обязателен на каждом POST
(подробнее — Идемпотентность).
# Загрузка файла: multipart/form-data, поле file. Idempotency-Key обязателен на каждом POST
MEDIA_ID=$(curl -sS --fail-with-body -X POST "$API_URL/v1/media" \
-H "$AUTH" -H "Idempotency-Key: $(uuidgen)" \
-F "file=@${MEDIA_FILE};type=image/jpeg" | jq -r '.media_id')
echo "media_id: $MEDIA_ID"
# Загрузка файла: multipart/form-data, поле file
boundary = uuid.uuid4().hex
filename = os.path.basename(media_path)
mime = mimetypes.guess_type(filename)[0] or "application/octet-stream"
with open(media_path, "rb") as file:
payload = (
f'--{boundary}\r\nContent-Disposition: form-data; name="file"; filename="{filename}"\r\n'
f"Content-Type: {mime}\r\n\r\n"
).encode() + file.read() + f"\r\n--{boundary}--\r\n".encode()
media = api(
"POST", "/v1/media",
data=payload, content_type=f"multipart/form-data; boundary={boundary}",
idempotency_key=str(uuid.uuid4()),
)
print("media_id:", media["media_id"])
// Загрузка файла: multipart/form-data, поле file
const form = new FormData();
form.append("file", new Blob([await readFile(mediaPath)], { type: "image/jpeg" }), basename(mediaPath));
const media = await api("POST", "/v1/media", { form, idempotencyKey: randomUUID() });
console.log("media_id:", media.media_id);
// Загрузка файла: multipart/form-data, поле file
$media = api('POST', '/v1/media', ['file' => new CURLFile($mediaPath, mime_content_type($mediaPath), basename($mediaPath))], multipart: true, idempotencyKey: uuid4());
echo 'media_id: ', $media['media_id'], "\n";
// Загрузка файла: multipart/form-data, поле file
file, err := os.ReadFile(mediaPath)
if err != nil {
fail("%v", err)
}
var form bytes.Buffer
writer := multipart.NewWriter(&form)
header := make(textproto.MIMEHeader)
header.Set("Content-Disposition", fmt.Sprintf(`form-data; name="file"; filename="%s"`, filepath.Base(mediaPath)))
header.Set("Content-Type", http.DetectContentType(file))
part, _ := writer.CreatePart(header)
_, _ = part.Write(file)
_ = writer.Close()
var media struct {
MediaID string `json:"media_id"`
}
api("POST", "/v1/media", form.Bytes(), writer.FormDataContentType(), uuid4(), &media)
fmt.Println("media_id:", media.MediaID)
7. Создайте пост
Пост — это подпись, медиа, каналы и время публикации. Здесь время — через 10 минут; о других вариантах
(next_slot, «сразу») — в разделе Посты и расписание.
# Создание поста. Повтор с тем же 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)
Ответ 201 содержит id поста и статус scheduled: пост принят и будет опубликован в назначенное время.
8. Узнайте результат
Статус поста и каждой его цели:
# Статус по каждой цели (каналу): scheduled -> dispatched -> published | failed ...
curl -sS --fail-with-body -H "$AUTH" "$API_URL/v1/posts/$POST_ID" \
| jq -r '"status: \(.status)", (.targets[] | "target: \(.channel_id) \(.status) \(.permalink // "-")")'
# Статус по каждой цели (каналу): scheduled -> dispatched -> published | failed ...
post = api("GET", f"/v1/posts/{post['id']}")
for target in post["targets"]:
print("target:", target["channel_id"], target["status"], target["permalink"] or "-")
// Статус по каждой цели (каналу): scheduled -> dispatched -> published | failed ...
const current = await api("GET", `/v1/posts/${post.id}`);
for (const target of current.targets) {
console.log("target:", target.channel_id, target.status, target.permalink ?? "-");
}
// Статус по каждой цели (каналу): scheduled -> dispatched -> published | failed ...
$current = api('GET', '/v1/posts/'.$post['id']);
foreach ($current['targets'] as $target) {
echo 'target: ', $target['channel_id'], ' ', $target['status'], ' ', $target['permalink'] ?? '-', "\n";
}
// Статус по каждой цели (каналу): scheduled -> dispatched -> published | failed ...
api("GET", "/v1/posts/"+post.ID, nil, "", "", &post)
for _, t := range post.Targets {
link := "-"
if t.Permalink != nil {
link = *t.Permalink
}
fmt.Println("target:", t.ChannelID, t.Status, link)
}
И лента событий проекта — удобный способ узнавать об итогах без опроса каждого поста:
# Лента событий проекта: храните next_after и передавайте его как after
curl -sS --fail-with-body -H "$AUTH" "$API_URL/v1/events?after=0&limit=50" \
| jq -r '(.data[] | "event: \(.seq) \(.type) \(.data.post_id // "")"), "next_after: \(.next_after)"'
# Лента событий проекта: храните next_after и передавайте его как after
feed = api("GET", "/v1/events?after=0&limit=50")
for event in feed["data"]:
print("event:", event["seq"], event["type"], event["data"].get("post_id", ""))
print("next_after:", feed["next_after"])
// Лента событий проекта: храните next_after и передавайте его как after
const feed = await api("GET", "/v1/events?after=0&limit=50");
for (const event of feed.data) {
console.log("event:", event.seq, event.type, event.data.post_id ?? "");
}
console.log("next_after:", feed.next_after);
// Лента событий проекта: храните next_after и передавайте его как after
$feed = api('GET', '/v1/events?after=0&limit=50');
foreach ($feed['data'] as $event) {
echo 'event: ', $event['seq'], ' ', $event['type'], ' ', $event['data']['post_id'] ?? '', "\n";
}
echo 'next_after: ', $feed['next_after'], "\n";
// Лента событий проекта: храните next_after и передавайте его как after
var feed struct {
Data []struct {
Seq int `json:"seq"`
Type string `json:"type"`
Data map[string]any `json:"data"`
} `json:"data"`
NextAfter int `json:"next_after"`
}
api("GET", "/v1/events?after=0&limit=50", nil, "", "", &feed)
for _, e := range feed.Data {
fmt.Println("event:", e.Seq, e.Type, e.Data["post_id"])
}
fmt.Println("next_after:", feed.NextAfter)
В ленте появятся post.published (с ссылкой на публикацию), post.failed или другие события —
см. Статусы и события.
Весь сценарий одной командой
Все шаги собраны в один скрипт на каждом языке. Клонируйте castbase-examples и запустите:
| Язык | Команда |
|---|---|
| curl | ./curl/publish.sh |
| Python | python3 python/publish.py |
| JavaScript | node javascript/publish.mjs |
| PHP | php php/publish.php |
| Go | cd go && go run . |
Что дальше
- Аутентификация и проекты и ключи;
- Особенности Instagram: карусель, Reels, обложка, квота публикаций;
- Claude Code, Codex и MCP: подключить агента одной строкой;
- Справочник API — все эндпоинты с примерами запросов и ответов.