Перейти к основному содержимому

Facebook Qubix Bridge

Facebook Qubix Bridge — это SDK, через который пользовательский скрипт обращается к Facebook напрямую: скрипт отправляет собственный запрос — в Graph API или на любой другой адрес *.facebook.com, — а Qubix проводит его через браузерный профиль, который вы уже подключили: с ключом доступа этого профиля, его куками, его прокси и подписью его браузера. Ответ Facebook возвращается скрипту как есть — вплоть до двоичного тела. Всё, что профилю позволено делать в рекламном кабинете руками, скрипт теперь может делать по расписанию: читать настройки и статистику, менять бюджеты, останавливать и запускать, выкачивать креативы, чистить комментарии.

Настраивать ничего не нужно: профиль подключён к Qubix — мост уже работает. Что запросу позволено — решают права этого профиля внутри Facebook: если профиль не может править кампанию руками, Facebook откажет и скрипту, и скрипт увидит этот отказ слово в слово.

Это труба, и только труба: адрес, способ, доводы и заголовки пишете вы, а ключ, куки, прокси и подпись браузера подставляет Qubix. Готовых функций управления кампаниями здесь нет намеренно — они вмораживали бы сегодняшнюю форму Facebook в поставку и ломались бы при первой же его правке. Высокоуровневое берите из образцов и правьте под себя: мост о форме запроса не знает ничего, поэтому не устаревает.

Как идёт запрос

Команда fb доступна в пользовательских скриптах — и по расписанию, и по кнопке ▶ Запустить. В правилах Britva её нет.

Два способа выбрать профиль-исполнитель

Каждый запрос отправляет какой-то браузерный профиль — исполнитель. Вы либо называете его сами, либо доверяете выбор Qubix.

JavaScript
// а) выбор за 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 подбирает среди профилей, которые видят эту кампанию, предпочитая профили владельца объявления. Если кандидат отпал на уровне доставки — умер прокси, оборвалась связь, — пробуется следующий, но только для чтения: изменение (POST, DELETE) через другой профиль не повторяется никогда, поэтому не может примениться дважды. Как только Facebook ответил — пусть даже отказом, — перебор останавливается. У цели есть и getProfiles() — тот же перечень профилей, что у кампании из выборки.
  • fb.profile(profileId) — запрос идёт ровно через этот профиль, без запасных. Цель несёт и поля самого профиля (name, tokenAlive, onCheckpoint, …) — так цели легко отличать друг от друга, когда вы перебираете несколько.

Какие профили видят объект

Прежде чем что-то отправлять, скрипт может спросить, какие профили могут работать с кампанией или объявлением, — со всем, что Qubix знает об их состоянии:

JavaScript
const profiles = fb.campaign(campaignId).getProfiles()
for (const p of profiles) {
console.log(p.name, 'ключ жив:', p.tokenAlive, 'на проверке:', p.onCheckpoint)
}

Возвращается массив: одну кампанию часто видят несколько профилей, уровни доступа у них в Facebook разные, и выбор — за вами. В каждой записи:

ПолеЗначение
idидентификатор профиля — его передают в fb.profile(...)
nameимя профиля
groupгруппа профиля
ownerBuyerIdзакупщик — владелец профиля
tokenAliveжив ли ключ доступа
onCheckpointне стоит ли профиль на проверочной странице Facebook
hasProxyнастроен ли у профиля прокси

Поля «уровень доступа» здесь нет намеренно: Qubix нигде его не хранит. Что профиль может сделать с объектом, отвечает сам Facebook — пошлите запрос и прочитайте ответ. tokenAlive, onCheckpoint и hasProxy — состояние, а не разрешение: профиль с мёртвым ключом в перечне остаётся, чтобы вы это увидели и решили сами. Объект вне вашего набора данных возвращает пустой массив, а не ошибку.

Запрос

Есть и плоская форма — fb.request({ ...настройки, profileId }) либо campaignId прямо в настройках: тот же запрос без отдельной цели.

request(options) принимает один объект:

ПараметрЗначение
urlабсолютный адрес запроса: `https://graph.facebook.com/${campaignId}`, https://graph.facebook.com/me/adaccounts. Годится любой хост *.facebook.com (https) — куки и ключ профиля на чужой хост не уходят, а ответные куки с такого адреса в сессию профиля не поглощаются
methodGET (по умолчанию), POST или DELETE
paramsдоводы запроса объектом; POST несёт их телом, остальные способы — в адресе
headersсвои заголовки; идут поверх наших и вправе затереть любой
timeoutMsсколько ждать ответа, в миллисекундах; пусто — срок оператора, выше его потолка не подняться

Ключ доступа, куки, прокси и подпись браузера подставляет транспорт профиля сам.

Ответ — и два вида отказа

Ответ Facebook приходит как есть, Qubix его не разбирает — разбираете вы:

JavaScript
const res = fb.campaign(campaignId).request({
method: 'GET',
url: `https://graph.facebook.com/${campaignId}`,
params: { fields: 'name,daily_budget' },
timeoutMs: 5000,
})

const data = JSON.parse(res.text)
if (res.status >= 400) { // отказ Facebook — обычный ответ с причиной в теле
console.log('Facebook отказал:', data.error.message, 'код', data.error.code)
return
}
console.log(data.name, 'дневной бюджет:', data.daily_budget)
ПолеЗначение
statusкод ответа HTTP от Facebook
contentTypeтип содержимого ответа
headersзаголовки ответа — все, кроме Set-Cookie; здесь живёт то, чего в теле нет: остаток лимитов, опознаватель обращения для разбора с поддержкой Facebook
textтело как есть, строкой — JSON.parse(res.text)
bytesоно же двоично — для картинок и выгрузок
profileIdкаким браузерным профилем ушёл запрос

Поля ok нет намеренно — оно могло быть только истиной и молча прятало бы отказы. Исход судите по status.

Отказов два вида, и они приходят по-разному:

  • Qubix не смог доставить — мёртвый прокси, оборванная связь, кампания вне вашего набора данных — бросается ошибкой: ловите try/catch, если хотите продолжить цикл. Для чтений через fb.campaign(...) к этому моменту уже перепробованы запасные профили.
  • Facebook ответил отказом — нет прав, неверное поле, протухшая сессия — возвращается обычным ответом с res.status >= 400 и причиной в теле: JSON.parse(res.text).error несёт message и code, а когда есть error_user_title и error_user_msg — это готовая человеческая формулировка.

Помощник на три строки, чтобы не повторять разбор в каждом рецепте:

JavaScript
function graph(res) {
if (res.status >= 400) throw new Error('Facebook отказал: ' + res.status)
return res.text ? JSON.parse(res.text) : {}
}

Проверка status стоит до разбора тела намеренно: Facebook не всегда отвечает JSON — страница проверки безопасности или заглушка посредника приходят разметкой, и JSON.parse на ней дал бы невнятную ошибку разбора вместо понятного отказа. Причина отказа словами — в теле: JSON.parse(res.text).error.message.

Не перечитывайте сразу после изменения

Чтение Facebook отдаёт с задержкой: значение, прочитанное сразу после удачного изменения, может оказаться прежним. Не считайте это неудачей записи. Проверяйте новое значение следующим прогоном либо запоминайте, что поставили, в ctx.state.

Пачка: много запросов одним обменом

Очередь по браузерному профилю пропускает один запрос в промежуток — сотня одиночных чтений в предел прогона не уложится. Пачка платит очередь один раз:

JavaScript
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(цель, подзапросы, { timeoutMs }) (третий довод необязателен) — отправляет подзапросы одним обменом; режется по 50 сама — это предел Facebook. Порядок сохраняется; у каждого ряда свой code (отказ — code >= 400), сырое тело в body и разобранный ответ в data (null, если тело не JSON).
  • Подзапрос — { method, relativeUrl, body }; тело строкой: 'daily_budget=2000'. Отказ всей пачки бросается.
  • Изменения в пачке класть можно, но пачка никогда не переходит к запасному профилю: оборванная связь оставит вас в неведении, применилось ли изменение, — перечитайте объект отдельным вызовом.
  • Двоичного у подответов не бывает: bytes есть только у одиночного запроса — картинки и выгрузки берите отдельным вызовом.

Пределы и журнал

  • Время прогона скрипта режет вызовы fb наравне с остальным кодом — команда синхронна, как sql и ctx.fetch.
  • Ожидание одного вызова задаёт администратор в СистемаJavaScript, блок Facebook (свой запрос через fb.*): умолчание и потолок. Ваш timeoutMs действует в границах потолка.
  • Одна очередь на профиль. Запросы к одному браузерному профилю разводятся по времени на весь сервер сразу — Britva, сбор статистики и скрипты делят одну очередь, поэтому скрипт не может сжечь профиль частыми обращениями. Нужно много чтений — берите пачку.
  • Пачка журналируется ОДНОЙ записью на обмен — обмен с Facebook у неё один; в колонке пути лежит перечень подзапросов, в теле ответа — все подответы.
  • Каждое обращение записывается в журнал запросов на вашем сервере — включая чтения: время, скрипт, профиль-исполнитель, способ, путь, исход и ответ Facebook. Жалоба «скрипт снёс мне кампании» разбирается по журналу за минуту. Ключи доступа в журнал не попадают никогда — из сохраняемых текстов они вычищаются.

Каталог кирпичиков

Каждый запрос ниже Qubix отправлял в живой работе и получал ответ (сверено 27.08.2026) — на них можно опираться как на базовый набор. Всё за его пределами тоже проходит: мост принимает любой адрес *.facebook.com — Graph API и не только, — а дальше решают права вашего профиля. AI-ассистент в редакторе скриптов знает эти кирпичики и соберёт скрипт по задаче, описанной словами.

Готовые скрипты, собранные из этих кирпичиков, — в разделе Примеры скриптов.

Кто я под этим профилем

JavaScript
const res = fb.profile(profileId).request({ url: 'https://graph.facebook.com/me', params: { fields: 'id,name,email' } })

Отказ здесь означает, что сессия профиля мертва. Это самый дешёвый способ проверять живость сессий по расписанию — раньше, чем это заметит залив.

Рекламные кабинеты — включая деньги

JavaScript
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причина отключения числом
currencyвалюта кабинета
timezone_nameчасовой пояс кабинета; «сегодня» считается по нему

Карты оплаты кабинетов

Тот же адрес, другой набор полей — в display_string приходит маска привязанной карты:

JavaScript
const res = fb.profile(profileId).request({
url: 'https://graph.facebook.com/me/adaccounts',
params: { limit: 100, fields: 'account_id,all_payment_methods{pm_credit_card{display_string}}' },
})

Состояние и бюджет кампании

JavaScript
const res = fb.campaign(campaignId).request({
url: `https://graph.facebook.com/${campaignId}`,
params: { fields: 'id,name,status,effective_status,daily_budget' },
})

daily_budget возвращается в минорных единицах валюты кабинета, строкой. Если бюджет задан на группах объявлений, а не на кампании, поля просто нет — это обычное состояние, а не ошибка.

Изменение бюджета

JavaScript
const res = fb.campaign(campaignId).request({
method: 'POST',
url: `https://graph.facebook.com/${campaignId}`,
params: { daily_budget: '5000' }, // минорные единицы: 5000 = 50.00 в валюте кабинета
})

На успех Facebook отвечает {"success":true}. Полный сценарий с порогами защиты — рецепт «Повышение бюджета прибыльных кампаний» в примерах.

Настройки группы объявлений: таргетинг, бюджеты, оптимизация

У группы объявлений читается то, чего нет в наших отчётах вовсе:

JavaScript
const res = fb.campaign(campaignId).request({
url: `https://graph.facebook.com/${adsetId}`,
params: { fields: 'name,targeting,daily_budget,lifetime_budget,optimization_goal,billing_event' },
})

Тем же способом у объявления одним запросом достаются вложенные объекты — кампания, группа и креатив сразу:

JavaScript
const res = fb.campaign(campaignId).request({
url: `https://graph.facebook.com/${adId}`,
params: { fields: 'name,status,effective_status,campaign{objective,daily_budget},adset{targeting,optimization_goal},creative{id}' },
})

Пауза и запуск

JavaScript
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' } })

Одна и та же форма работает для объявления, группы и кампании — Facebook различает их по идентификатору. Пауза кампании действует каскадом: её объявления показывают effective_status: 'CAMPAIGN_PAUSED', а их собственный status не меняется. Поэтому о том, крутится ли объявление на самом деле, судите по effective_status, а не по status.

Для пауз объявлений лучше подходят команды SDK

Останавливать и возвращать объявления, группы и кампании удобнее встроенными .pause() / .activate(): они идут через ту же очередь, что и Britva, и ведут полный учёт пауз. fb берите для того, на что у SDK нет своей команды.

Почему объявление не крутится

JavaScript
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 ловит истёкшую группу — обычный ответ на «объявление активно, а расхода нет».

Статистика напрямую из кабинета

Когда нужен срез, которого нет в отчётах Qubix, статистику можно спросить у самого Facebook:

JavaScript
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 и unique_outbound_clicks приходят массивом объектов action_type/value, а не числом, — остальные клик-поля приходят числом строкой;
  • поля landing_page_views на верхнем уровне нет вовсе — запросите actions и ищите в нём action_type: 'landing_page_view'.

Картинки креативов — вплоть до двоичного тела

JavaScript
const res = fb.campaign(campaignId).request({
url: `https://graph.facebook.com/act_${accountId}/adimages`,
params: { hashes: JSON.stringify([imageHash]), fields: 'url,permalink_url,hash' },
})

Ответ fb умеет быть и двоичным: res.bytes несёт тело как есть — так забирают сами файлы, а не только ссылки на них.

Исходник видео и картинка поста

JavaScript
// исходный файл видео креатива
fb.campaign(campaignId).request({ url: `https://graph.facebook.com/${videoId}`, params: { fields: 'source' } })

Цепочка «объявление → его картинка» — три зависимых шага: каждому нужен ответ предыдущего, поэтому в один запрос она не складывается. Зато она складывается в пачки по шагам — по целому списку объявлений разом: три обмена на всех, а не три на каждое:

JavaScript
const ads = fb.batch(fb.campaign(adIds[0]), adIds.map((id) => ({ method: 'GET', relativeUrl: `${id}?fields=creative{id}` })))
const creativeIds = ads.filter((r) => r.code < 400).map((r) => r.data.creative.id)

const creatives = fb.batch(fb.campaign(adIds[0]), creativeIds.map((id) => ({ method: 'GET', relativeUrl: `${id}?fields=effective_object_story_id` })))
const postIds = creatives.filter((r) => r.code < 400).map((r) => r.data.effective_object_story_id)

const posts = fb.batch(fb.campaign(adIds[0]), postIds.map((id) => ({ method: 'GET', relativeUrl: `${id}?fields=full_picture` })))
for (const r of posts) if (r.code < 400) console.log(r.data.full_picture)

Страницы, бизнес-менеджеры, посты и комментарии

JavaScript
// фан-страницы и бизнес-менеджеры — независимые чтения, один обмен
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}` })
Чистку комментариев Qubix умеет и сам

Автоматическая чистка подсадных ссылок с журналом уже встроена — раздел Чистка комментариев. Через fb стройте собственную логику поверх: свои списки слов, свои исключения, свой разбор авторов.

Постраничный обход

Длинные списки Facebook отдаёт курсорами: пока в ответе есть paging.next, берите paging.cursors.after и повторяйте запрос с after в params. Нет paging — значит конец.

JavaScript
let after = null
do {
const params = { limit: 50, fields: 'name,account_id' }
if (after) params.after = after
const data = JSON.parse(fb.profile(profileId).request({ url: 'https://graph.facebook.com/me/adaccounts', params: params }).text)
if (data.error) break
for (const acc of data.data || []) console.log(acc.name)
after = data.paging && data.paging.next ? data.paging.cursors.after : null
} while (after)

Если Facebook отвечает «Please reduce the amount of data you're asking for» — это просьба, а не отказ: уменьшите limit и начните заново.

Сила — в связках с остальным SDK

Мост становится по-настоящему мощным вместе с остальными командами скрипта: условия withCondition отбирают объекты по вашей статистике, sql достаёт любой срез из базы, fb сверяет и меняет кабинет, ctx.state держит память между прогонами, ctx.fetch уводит итог в ваш мессенджер.

JavaScript
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)
}
}

Отбирать объекты можно и без SQL — условием withCondition: это выражение уровня SQL по всем показателям объекта, со скобками, AND/OR/NOT, арифметикой и сравнением полей между собой — spend_24h > 2 * geo_avg_payout, roas_24h < 0.5 * prev_roas_24h. Полный перечень полей — вкладка макросов в редакторе и раздел Метрики.

Так собираются сценарии, до которых у поставщика никогда не дошли бы руки: ваша воронка решает, ваш профиль исполняет.

Что дальше