Facebook теперь программируется. Целиком
Всё, что вы делаете в рекламном кабинете руками, теперь умеет делать скрипт — вашим же профилем, по расписанию, круглые сутки.
Это Facebook Qubix Bridge — прямой мост в Facebook из скриптов Qubix. Скрипт отправляет собственный запрос к Facebook — в Graph API или на любой другой его адрес, — а Qubix проводит его через ваш браузерный профиль: ключ, куки, прокси, подпись браузера. Задумка, которая вчера ждала чужой очереди доработок, сегодня — короткий скрипт, который ассистент напишет за вас.
🎬 GIF: скрипт читает дневной бюджет кампании, поднимает его, изменение видно в рекламном кабинете
Что это открывает
🧠 Ваша воронка решает — ваш профиль исполняет. Скрипт прямым SQL-запросом считает окупаемость по вашей статистике — депозиты, доход, любой срез вплоть до гео и часа — и по ней двигает кабинет. Сам кабинет такого не умеет в принципе: вашей воронки у него нет.
💰 Бюджеты по вашей логике. Скрипт находит кампании с окупаемостью выше порога, читает их живой бюджет из самого Facebook и поднимает на ваш шаг — с потолком и выдержкой, чтобы рост не разгонялся и не сбивал обучение. Готовый образец «повышение бюджета прибыльных кампаний» уже в поставке: скопируйте и подстройте пороги под себя.
🔍 Диагностика словами самого Facebook. «Объявление активно, а расхода нет» — скрипт спрашивает у Facebook напрямую и приносит его собственную формулировку проблемы: не прошла модерация, истекла группа, ограничен кабинет.
💳 Деньги кабинетов — сводкой. Общий расход, суточный предел, порог списания, привязанная карта — по каждому кабинету, по расписанию, прямо в консоль или сообщением вам в мессенджер.
🎯 Настройки, которых нет в отчётах. Таргетинг групп, цели оптимизации, вложенные настройки объявления — читаются одним запросом.
📦 Сотни объектов — одним обменом. Пачка запросов уходит в Facebook разом: бюджеты всех кампаний читаются за один заход, а не по одному.
🖼 Креативы — к себе. Картинки и исходники видео выкачиваются скриптом — вплоть до самих файлов — и остаются у вас.
🧹 Комментарии — по вашим правилам. Автоматическая чистка подсадных ссылок в Qubix уже есть; мост добавляет к ней вашу собственную логику: свои списки слов, свои исключения.
📊 Статистика кабинета любым срезом — когда нужен разрез, которого нет в готовых отчётах, скрипт спрашивает его у Facebook сам.
И это только начало списка: мост принимает любой адрес Facebook — Graph API и не только, вплоть до двоичных выгрузок. Ваша фантазия здесь не ограничена ничем — ограничены только права вашего же профиля в Facebook.
Как это устроено: труба, и только труба
Адрес, способ, доводы и заголовки пишете вы, а ключ, куки, прокси и подпись браузера подставляет Qubix. Готовых функций управления кампаниями в мосте нет намеренно — они вмораживали бы сегодняшнюю форму Facebook в поставку и ломались бы при первой же его правке. Мост о форме запроса не знает ничего — поэтому не устаревает: меняется поле у Facebook — вы правите строку в своём скрипте, не дожидаясь обновления Qubix.
Команда fb доступна в пользовательских скриптах — и по расписанию, и по кнопке
▶ Запустить.
Два способа выбрать профиль-исполнитель
Каждый запрос отправляет какой-то браузерный профиль — исполнитель. Вы либо называете его сами, либо доверяете выбор Qubix.
// а) выбор за Qubix: любой профиль, который видит эту кампанию
const res = fb.campaign(campaignId).request({
method: 'GET',
url: `https://graph.facebook.com/${campaignId}`,
params: { fields: 'name,daily_budget' },
})
// б) названный профиль: без подбора и без запасных
const res2 = fb.profile(profileId).request({
method: 'GET',
url: 'https://graph.facebook.com/me',
params: { fields: 'id,name' },
})
fb.campaign(campaignId)— исполнителя Qubix подбирает среди профилей, которые видят эту кампанию, предпочитая профили владельца объявления. Отпал кандидат на уровне доставки — пробуется следующий, но только для чтения: изменение через другой профиль не повторяется никогда, поэтому не может примениться дважды. Facebook ответил — пусть даже отказом — перебор останавливается.fb.profile(profileId)— запрос идёт ровно этим профилем, без запасных. Перечень профилей, видящих кампанию или объявление, даётgetProfiles()— со всем, что мы о них знаем: имя, группа, владелец, жив ли ключ, не стоит ли профиль на проверке безопасности.
Запрос и ответ
request(options) принимает один объект:
| Параметр | Значение |
|---|---|
url | абсолютный адрес запроса: `https://graph.facebook.com/${campaignId}`, https://graph.facebook.com/me/adaccounts. Годится любой хост *.facebook.com — куки и ключ профиля на чужой хост не уходят |
method | GET (по умолчанию), POST или DELETE |
params | доводы запроса объектом; POST несёт их телом, остальные способы — в адресе |
headers | свои заголовки; идут поверх наших и вправе затереть любой |
timeoutMs | сколько ждать ответа; пусто — срок оператора, выше его потолка не подняться |
Ответ Facebook приходит как есть, Qubix его не разбирает — разбираете вы: status (поля ok нет намеренно — оно могло быть только истиной и прятало бы отказы),
contentType, headers (все, кроме Set-Cookie), text (тело строкой — JSON.parse(res.text)),
bytes (оно же двоично — для картинок и выгрузок), profileId (каким профилем ушло).
Отказов два вида: Qubix не смог доставить — бросается ошибкой, ловите try/catch;
Facebook ответил отказом — обычный ответ со status >= 400 и причиной в теле. Помощник на
три строки, чтобы не повторять разбор:
function graph(res) {
if (res.status >= 400) throw new Error('Facebook отказал: ' + res.status)
return res.text ? JSON.parse(res.text) : {}
}
Чтение Facebook отдаёт с задержкой: значение, прочитанное сразу после удачного изменения,
может оказаться прежним. Проверяйте следующим прогоном либо запоминайте поставленное в
ctx.state.
Пачка: много запросов одним обменом
Очередь по браузерному профилю пропускает один запрос в промежуток — сотня одиночных чтений в предел прогона не уложится. Пачка платит очередь один раз:
const rows = fb.batch(fb.profile(profileId), ids.map((id) => ({ method: 'GET', relativeUrl: `${id}?fields=name,daily_budget` })))
for (const r of rows) {
if (r.code >= 400) { console.log('код', r.code); continue }
console.log(r.data.name, r.data.daily_budget)
}
fb.batch режется по 50 подзапросов сама — это предел Facebook; порядок сохраняется, у каждого ряда свой code (отказ — code >= 400), разобранный ответ — в data. Подзапрос — { method, relativeUrl, body }; перечень чтений собирается обычным map. Пачка никогда не переходит к запасному профилю — изменение внутри неё применяется один раз либо не применяется вовсе.
Каталог кирпичиков
Каждый запрос ниже Qubix отправлял в живой работе и получал ответ (сверено 27.08.2026) — на
них можно опираться как на базовый набор. Всё за пределами тоже проходит: мост принимает
любой адрес *.facebook.com, а дальше решают права вашего профиля.
Кто я под этим профилем
const res = fb.profile(profileId).request({ url: 'https://graph.facebook.com/me', params: { fields: 'id,name,email' } })
Отказ здесь означает, что сессия профиля мертва, — самый дешёвый способ проверять живость сессий по расписанию, раньше, чем это заметит залив.
Рекламные кабинеты — включая деньги
const res = fb.profile(profileId).request({
url: 'https://graph.facebook.com/me/adaccounts',
params: {
limit: 50,
fields: 'name,account_id,account_status,disable_reason,currency,' +
'adspaymentcycle,adtrust_dsl,amount_spent,timezone_name',
},
})
Деньги живут здесь — отдельного запроса «баланс» у Graph API нет: amount_spent — общий
расход в минорных единицах валюты кабинета, adtrust_dsl — суточный предел,
adspaymentcycle — порог списания (сумма в .data[0].threshold_amount, делится на 100),
account_status и disable_reason — состояние и причина отключения числами. Карты оплаты —
тем же адресом с полем all_payment_methods{pm_credit_card{display_string}}.
Состояние и бюджет кампании
const res = fb.campaign(campaignId).request({
url: `https://graph.facebook.com/${campaignId}`,
params: { fields: 'id,name,status,effective_status,daily_budget' },
})
daily_budget приходит в минорных единицах, строкой. Бюджет задан на группах — поля просто
нет, это обычное состояние, а не ошибка.
Изменение бюджета
const res = fb.campaign(campaignId).request({
method: 'POST',
url: `https://graph.facebook.com/${campaignId}`,
params: { daily_budget: '5000' }, // минорные единицы: 5000 = 50.00 в валюте кабинета
})
На успех Facebook отвечает {"success":true}.
Настройки группы: таргетинг, бюджеты, оптимизация
const res = fb.campaign(campaignId).request({
url: `https://graph.facebook.com/${adsetId}`,
params: { fields: 'name,targeting,daily_budget,lifetime_budget,optimization_goal,billing_event' },
})
Тем же способом у объявления одним запросом достаются вложенные объекты — кампания, группа и
креатив сразу: fields: 'name,effective_status,campaign{objective,daily_budget},adset{targeting},creative{id}'.
Пауза и запуск
fb.campaign(campaignId).request({ method: 'POST', url: `https://graph.facebook.com/${someId}`, params: { status: 'PAUSED' } })
fb.campaign(campaignId).request({ method: 'POST', url: `https://graph.facebook.com/${someId}`, params: { status: 'ACTIVE' } })
Одна форма для объявления, группы и кампании. Пауза кампании действует каскадом: у её
объявлений effective_status становится CAMPAIGN_PAUSED, а собственный status не
меняется — судите по effective_status.
Почему объявление не крутится
const res = fb.campaign(campaignId).request({
url: `https://graph.facebook.com/${adId}`,
params: { fields: 'effective_status,issues_info{error_summary,error_message,level},adset{effective_status,end_time}' },
})
issues_info несёт формулировку проблемы словами самого Facebook; adset.end_time ловит
истёкшую группу.
Статистика напрямую из кабинета
const res = fb.campaign(campaignId).request({
url: `https://graph.facebook.com/act_${accountId}/insights`,
params: {
level: 'ad',
fields: 'ad_id,ad_name,spend,impressions,clicks',
time_range: JSON.stringify({ since: '2026-08-20', until: '2026-08-27' }),
time_increment: 1,
limit: 500,
},
})
Два подводных камня: outbound_clicks приходит массивом объектов, а поля
landing_page_views на верхнем уровне нет вовсе — ищите его внутри actions.
Креативы, страницы, комментарии
// картинки по хешам
fb.campaign(campaignId).request({ url: `https://graph.facebook.com/act_${accountId}/adimages`, params: { hashes: JSON.stringify([imageHash]), fields: 'url,permalink_url,hash' } })
// исходник видео
fb.campaign(campaignId).request({ url: `https://graph.facebook.com/${videoId}`, params: { fields: 'source' } })
// фан-страницы и бизнес-менеджеры — независимые чтения, один обмен
const rows = fb.batch(fb.profile(profileId), [
{ method: 'GET', relativeUrl: 'me/accounts?limit=25' },
{ method: 'GET', relativeUrl: 'me/businesses' },
])
// посты страницы
const posts = graph(fb.profile(profileId).request({ url: `https://graph.facebook.com/${pageId}/published_posts`, params: { limit: 5 } }))
// комментарии ВСЕХ свежих постов — одной пачкой на весь список
const coms = fb.batch(fb.profile(profileId),
(posts.data || []).map((po) => ({ method: 'GET', relativeUrl: `${po.id}/comments?fields=id,message,from,created_time&limit=100` })))
// удалить комментарий — изменение идёт одиночным запросом
fb.profile(profileId).request({ method: 'DELETE', url: `https://graph.facebook.com/${commentId}` })
Ответ fb умеет быть и двоичным: res.bytes несёт тело как есть — так забирают сами файлы,
а не только ссылки на них. Длинные списки Facebook отдаёт курсорами: пока в ответе есть
paging.next, берите paging.cursors.after и повторяйте запрос.
Сила — в связках с остальным SDK
Условия отбирают объекты по вашей статистике, sql достаёт любой срез из базы, fb сверяет
и меняет кабинет, ctx.state держит память между прогонами, ctx.fetch уводит итог в ваш
мессенджер:
function main() {
// 1. Своя статистика — прямой ClickHouse-запрос: окупаемость кампаний по доходу ВОРОНКИ
const rows = sql`
SELECT campaign_id, sum(spend_24h) AS spend, sum(revenue_24h) AS revenue
FROM v_ads_stats
GROUP BY campaign_id
HAVING spend > 0 AND revenue / spend >= 1.5
ORDER BY revenue / spend DESC`
// 2. Живые бюджеты всех кандидатов — одним обменом с Facebook
const ids = rows.map((r) => r.campaign_id)
if (!ids.length) return
const budgets = fb.batch(fb.campaign(ids[0]), ids.map((id) => ({ method: 'GET', relativeUrl: `${id}?fields=name,daily_budget` })))
for (let i = 0; i < ids.length; i++) {
if (budgets[i].code >= 400) continue
console.log(budgets[i].data.name,
'ROAS воронки:', (rows[i].revenue / rows[i].spend).toFixed(2),
'бюджет в кабинете:', budgets[i].data.daily_budget)
}
}
Так собираются сценарии, до которых у поставщика никогда не дошли бы руки: ваша воронка решает, ваш профиль исполняет.
Кода можно не писать вовсе
Опишите задачу словами — «каждое утро поднимай бюджет кампаниям, которые окупаются, но не выше потолка» — и AI-ассистент прямо в редакторе соберёт скрипт, вставит его в код и поможет поставить на расписание. Ассистент знает и мост, и вашу статистику: скрипт может сперва спросить вашу воронку — какие кампании принесли депозиты, — а потом сходить в Facebook и подвинуть их бюджеты.
🎬 GIF: задача словами в ассистенте → готовый скрипт в редакторе → кнопка запуска
Под контролем — каждый шаг
Каждое обращение скрипта в Facebook — включая чтения — записывается в журнал на вашем
сервере: какой скрипт, когда, каким профилем, что ответил Facebook. Изменение никогда не
повторяется через запасной профиль, поэтому не может примениться дважды. Запросы к одному
профилю разводятся по времени со всей остальной автоматикой — Britva, сбор статистики и
скрипты делят одну очередь, профиль в безопасности. Время прогона режет вызовы fb наравне
с остальным кодом, срок ожидания одного вызова ограничен потолком оператора, а ключи доступа
в журнал не попадают вовсе — из сохраняемых текстов они вычищаются.
Настраивать нечего
Скрипт ходит в Facebook вашим же браузерным профилем — тем самым, которым вы работаете в кабинете: его ключ, его куки, его прокси, его подпись браузера. Для платформы это выглядит как работа живого человека, а не автоматизация. Профиль подключён к Qubix — мост уже работает.
Раньше каждое «а можно, чтобы само?» упиралось в чужие руки. Теперь между вашей задумкой и работающей автоматикой — один скрипт, который ассистент напишет за вас.
Обновите Qubix — и отдайте рутину кабинета скриптам. 🚀