Przejdź do głównej zawartości

Facebook Qubix Bridge

Facebook Qubix Bridge to SDK, które pozwala skryptowi użytkownika rozmawiać bezpośrednio z Facebookiem: skrypt wysyła własne żądanie — do Graph API lub na dowolny inny adres *.facebook.com — a Qubix przenosi je przez już podłączony profil przeglądarki: z tokenem dostępu, ciasteczkami, proxy i sygnaturą przeglądarki tego profilu. Odpowiedź Facebooka wraca do skryptu bez zmian — łącznie z treścią binarną. Wszystko, co profil może zrobić ręcznie w panelu reklamowym, skrypt może teraz robić według harmonogramu: odczytywać ustawienia i statystyki, zmieniać budżety, wstrzymywać i uruchamiać, pobierać kreacje, czyścić komentarze.

Nie trzeba niczego konfigurować: skoro profil jest podłączony do Qubix, most już działa. To, co wolno zrobić danemu żądaniu, decydują uprawnienia tego profilu w samym Facebooku: jeśli profil nie może ręcznie edytować kampanii, Facebook odmówi także skryptowi, a skrypt zobaczy tę odmowę dosłownie.

To jest kanał — i tylko kanał: Pan/Pani zapisuje adres, metodę, parametry i nagłówki, a Qubix dostarcza klucz, ciasteczka, proxy i sygnaturę przeglądarki. Celowo nie ma tu gotowych funkcji do zarządzania kampaniami — zamroziłyby dzisiejszy kształt Facebooka w produkcie i przestałyby działać przy pierwszej jego zmianie. Proszę wziąć gotowe elementy wysokiego poziomu z przykładów i dopasować je do siebie: most nic nie wie o kształcie żądania — i właśnie dlatego się nie starzeje.

Jak przebiega żądanie

Polecenie fb jest dostępne w skryptach użytkownika — zarówno według harmonogramu, jak i przyciskiem ▶ Uruchom. Nie jest dostępne w regułach automatycznych Britva.

Dwa sposoby wyboru profilu wykonawcy

Każde żądanie jest wysyłane przez jakiś profil przeglądarki — wykonawcę. Może Pan/Pani wskazać go samodzielnie albo pozwolić, aby Qubix wybrał go sam.

JavaScript
// a) let Qubix pick: any profile that sees this campaign
const res = fb.campaign(campaignId).request({
method: 'GET',
url: `https://graph.facebook.com/${campaignId}`,
params: { fields: 'name,daily_budget' },
})

// b) a named profile: no picking, no fallbacks
const res2 = fb.profile(profileId).request({
method: 'GET',
url: 'https://graph.facebook.com/me',
params: { fields: 'id,name' },
})
  • fb.campaign(campaignId) — kampania musi należeć do zakresu danych Pana/Pani skryptu (do tego, co wolno widzieć Pana/Pani roli). Qubix wybiera wykonawcę spośród profili, które widzą tę kampanię, preferując profile właściciela reklamy. Jeśli kandydat odpada na poziomie dostarczenia — martwe proxy, zerwane połączenie — próbowany jest kolejny, ale tylko przy odczytach: zmiana (POST, DELETE) nigdy nie jest ponawiana przez inny profil, aby nie została zastosowana dwukrotnie. Gdy tylko Facebook odpowie — nawet odmową — poszukiwania się kończą. Cel niesie też getProfiles() — tę samą listę profili, którą ma kampania z Pana/Pani wyboru.
  • fb.profile(profileId) — żądanie idzie dokładnie przez ten profil, bez żadnych profili zapasowych. Cel niesie też własne pola profilu (name, tokenAlive, onCheckpoint, …), dzięki czemu łatwo odróżnić cele przy iterowaniu po kilku naraz.

Które profile widzą dany obiekt

Zanim cokolwiek wyśle, skrypt może zapytać, które profile mogą działać na kampanii lub reklamie — wraz ze wszystkim, co Qubix wie o ich stanie:

JavaScript
const profiles = campaign.getProfiles() // also: ad.getProfiles() and fb.campaign(id).getProfiles()
for (const p of profiles) {
console.log(p.name, 'token alive:', p.tokenAlive, 'checkpoint:', p.onCheckpoint)
}

Zwraca tablicę: jedna kampania jest często widoczna dla kilku profili, ich poziomy dostępu w Facebooku się różnią, a wybór należy do Pana/Pani. Każdy element zawiera:

PoleZnaczenie
ididentyfikator profilu — proszę przekazać go do fb.profile(...)
namenazwa profilu
groupgrupa profilu
ownerBuyerIdbuyer, który jest właścicielem profilu
tokenAliveczy token dostępu jest aktywny
onCheckpointczy profil utknął na weryfikacji bezpieczeństwa Facebooka (checkpoint)
hasProxyczy profil ma skonfigurowane proxy

Celowo nie ma tu pola „poziom dostępu" — Qubix nigdzie go nie przechowuje. Na to, co profil może zrobić z obiektem, odpowiada sam Facebook — proszę wysłać żądanie i przeczytać odpowiedź. tokenAlive, onCheckpoint i hasProxy to stan, a nie uprawnienie: profil z martwym tokenem pozostaje na liście, aby Pan/Pani go zobaczył i sam zdecydował. Obiekt spoza Pana/Pani zakresu danych zwraca pustą tablicę, a nie błąd.

Żądanie

request(options) przyjmuje jeden obiekt:

OpcjaZnaczenie
urlbezwzględny adres żądania: https://graph.facebook.com/${campaignId}, https://graph.facebook.com/act_123/campaigns, https://graph.facebook.com/me/adaccounts. Działa dowolny host *.facebook.com — most dołącza ciasteczka i klucz profilu, i nigdy nie trafiają one na obcy host
methodGET (domyślnie), POST lub DELETE
paramsparametry żądania jako obiekt; POST przenosi je w treści, pozostałe metody — w adresie
headerswłasne nagłówki; nakładają się na nasze i mogą nadpisać każdy z nich
timeoutMsjak długo czekać na odpowiedź, w milisekundach; puste — wartość domyślna operatora, a powyżej pułapu wyjść się nie da

Token dostępu, ciasteczka, proxy i sygnaturę przeglądarki dostarcza sam transport profilu.

Odpowiedź — i dwa rodzaje odmowy

Odpowiedź Facebooka przychodzi bez zmian; Qubix jej nie parsuje — robi to Pan/Pani:

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's refusal — a normal answer with the reason in the body
console.log('Facebook refused:', data.error.message, 'code', data.error.code)
return
}
console.log(data.name, 'daily budget:', data.daily_budget)
PoleZnaczenie
okodpowiedź dotarła, a kod jest sukcesem
statuskod odpowiedzi HTTP od Facebooka
contentTypetyp zawartości odpowiedzi
headersnagłówki odpowiedzi — wszystkie oprócz Set-Cookie; tu żyje to, czego nie ma w treści: pozostałe limity zapytań, identyfikator żądania do zgłoszenia w pomocy technicznej Facebooka
texttreść bez zmian, jako string — JSON.parse(res.text)
bytesta sama treść w postaci binarnej — do obrazów i pobierania plików
profileIdktóry profil przeglądarki wysłał żądanie

Istnieją dwa rodzaje odmowy, i przychodzą one różnie:

  • Qubix nie mógł dostarczyć żądania — martwe proxy, zerwane połączenie, kampania spoza Pana/Pani zakresu danych — jest zgłaszane jako błąd: proszę przechwycić go try/catch, jeśli pętla ma kontynuować działanie. Przy odczytach przez fb.campaign(...) profile zapasowe zostały już wypróbowane na tym etapie.
  • Facebook odpowiedział odmową — brak uprawnień, błędne pole, wygasła sesja — jest zwracane jako zwykła odpowiedź z res.status >= 400 i przyczyną w treści: JSON.parse(res.text).error niesie message i code, a gdy obecne są error_user_title i error_user_msg — jest to gotowe sformułowanie dla człowieka.

Trzylinijkowa funkcja pomocnicza, aby nie powtarzać parsowania w każdym przepisie:

JavaScript
function graph(res) {
const data = res.text ? JSON.parse(res.text) : {}
if (res.status >= 400) throw new Error('Facebook: ' + ((data.error && data.error.message) || res.status))
return data
}
Nie odczytuj od razu po zmianie

Facebook serwuje odczyty z opóźnieniem: wartość odczytana zaraz po udanej zmianie może wciąż być starą wartością. Proszę nie traktować tego jako nieudanego zapisu. Proszę sprawdzić nową wartość przy kolejnym uruchomieniu albo zapamiętać to, co zostało ustawione, w ctx.state.

Żądanie wsadowe: wiele żądań w jednej wymianie

Kolejka na profil przepuszcza jedno żądanie na interwał — sto pojedynczych odczytów nie zmieści się w limicie czasu uruchomienia. Żądanie wsadowe płaci za kolejkę raz:

JavaScript
const rows = fbBatch(fb.profile(profileId), fbReads(ids, 'name,daily_budget'))
for (const r of rows) {
if (r.code !== 200) { console.log('code', r.code); continue }
console.log(r.data.name, r.data.daily_budget)
}
  • fbBatch(target, subrequests, { timeoutMs }) — wysyła podżądania w jednej wymianie; dzieli je na porcje po 50 — taki jest limit Facebooka. Odmowa całego żądania wsadowego zgłaszana jest jako błąd; wynik każdego podżądania proszę sprawdzić w jego code, a jego sparsowaną odpowiedź — w data.
  • fbReads(ids, fields) — buduje podżądania odczytu z listy identyfikatorów; fields proszę napisać samodzielnie. Podżądanie można też zbudować ręcznie: { method: 'GET', relativeUrl: 'act_123/ads?fields=name' }.

Limity i dziennik

  • Czas działania skryptu obcina wywołania fb tak samo jak resztę kodu — polecenie jest synchroniczne, tak samo jak sql i ctx.fetch.
  • Czas oczekiwania na wywołanie ustawia administrator w SystemJavaScript, w bloku Facebook (own request via fb.*): wartość domyślna i pułap. Pana/Pani timeoutMs działa w granicach tego pułapu.
  • Jedna kolejka na profil. Żądania do jednego profilu przeglądarki są rozkładane w czasie w skali całej instalacji — Britva, zbieranie statystyk i skrypty dzielą jedną kolejkę, więc skrypt nie może „spalić" profilu częstymi wywołaniami. Potrzeba wielu odczytów — proszę użyć żądania wsadowego.
  • Każde żądanie jest zapisywane w dzienniku żądań na Pana/Pani serwerze — łącznie z odczytami: czas, skrypt, profil wykonawcy, metoda, ścieżka, wynik i odpowiedź Facebooka. Skargę w rodzaju „skrypt zniszczył moje kampanie" można wyjaśnić w minutę dzięki dziennikowi. Tokeny dostępu nigdy nie trafiają do dziennika — są usuwane z zapisywanych tekstów.

Katalog gotowych elementów

Każde z poniższych żądań zostało wysłane przez Qubix w działającej instalacji i doczekało się odpowiedzi (zweryfikowano 27 sierpnia 2026) — można na nie polegać jako na zestawie podstawowym. Wszystko poza nim także przejdzie: most akceptuje dowolny adres *.facebook.com — Graph API i nie tylko — a resztę decydują uprawnienia Pana/Pani profilu. Asystent AI w edytorze skryptów zna te elementy i złoży skrypt na podstawie zadania opisanego zwykłymi słowami.

Gotowe skrypty zbudowane z tych elementów znajdują się w Przykładach skryptów.

Kim jestem pod tym profilem

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

Odmowa tutaj oznacza, że sesja profilu jest martwa. To najtańszy sposób na sprawdzanie kondycji sesji według harmonogramu — wcześniej, niż zauważy to buyer.

Konta reklamowe — łącznie z pieniędzmi

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

Tu żyją pieniądze — Graph API nie ma osobnego endpointu „saldo":

PoleCo to jest
amount_spentłączne wydatki konta, w jednostkach pomocniczych waluty konta
adtrust_dsldzienny limit wydatków konta
adspaymentcyclepróg rozliczeniowy; kwota znajduje się w .data[0].threshold_amount i jest dzielona przez 100
account_statusstan konta, jako liczba
disable_reasonpowód wyłączenia, jako liczba
currencywaluta konta
timezone_namestrefa czasowa konta; w niej liczone jest „dziś"

Karty płatnicze kont

Ten sam adres, inny zestaw pól — display_string niesie zamaskowany numer podłączonej karty:

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

Stan i budżet kampanii

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

daily_budget jest zwracany w jednostkach pomocniczych waluty konta, jako string. Jeśli budżet jest ustawiony na zestawach reklam, a nie na kampanii, pole po prostu nie występuje — to normalny stan, a nie błąd.

Zmiana budżetu

JavaScript
const res = fb.campaign(campaignId).request({
method: 'POST',
url: `https://graph.facebook.com/${campaignId}`,
params: { daily_budget: '5000' }, // minor units: 5000 = 50.00 in the account currency
})

Przy powodzeniu Facebook odpowiada {"success":true}. Pełny scenariusz z progami bezpieczeństwa to przepis „Podnoszenie budżetu opłacalnych kampanii" w przykładach.

Ustawienia zestawu reklam: targetowanie, budżety, optymalizacja

Zestaw reklam ujawnia to, czego w ogóle nie ma w naszych raportach:

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

W ten sam sposób jedno żądanie na reklamie pobiera jej zagnieżdżone obiekty — kampanię, zestaw reklam i kreację naraz:

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

Wstrzymywanie i uruchamianie

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

Ta sama forma działa dla reklamy, zestawu reklam i kampanii — Facebook rozróżnia je po identyfikatorze. Wstrzymanie kampanii kaskaduje: jej reklamy zgłaszają effective_status: 'CAMPAIGN_PAUSED', podczas gdy ich własny status pozostaje bez zmian. Dlatego o tym, czy coś faktycznie działa, proszę oceniać po effective_status, nigdy po status.

Do wstrzymywania reklam lepiej użyć akcji SDK

Wstrzymywanie i przywracanie reklam, zestawów reklam i kampanii lepiej wykonywać wbudowanymi .pause() / .activate(): przechodzą one przez tę samą kolejkę co Britva i prowadzą pełną ewidencję wstrzymań. fb proszę używać do tego, na co SDK nie ma polecenia.

Dlaczego reklama nie jest wyświetlana

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 niesie problem słowami samego Facebooka; adset.end_time wyłapuje wygasły zestaw reklam — zwykłą odpowiedź na pytanie „reklama jest aktywna, ale nic się nie wydaje".

Statystyki prosto z panelu reklamowego

Gdy potrzebny jest wycinek, którego nie mają raporty Qubix, proszę zapytać sam 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, // by day
limit: 500,
},
})

Dwie pułapki, w które wpada każdy:

  • outbound_clicks i unique_outbound_clicks przychodzą jako tablica obiektów action_type/value, a nie jako liczba — pozostałe pola kliknięć przychodzą jako liczba w stringu;
  • pola landing_page_views na najwyższym poziomie w ogóle nie ma — proszę zażądać actions i poszukać w nim action_type: 'landing_page_view'.

Obrazy kreacji — łącznie z treścią binarną

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

Odpowiedź fb też może być binarna: res.bytes niesie treść bez zmian — tak właśnie pobiera się same pliki, a nie tylko linki do nich.

Źródło wideo i zdjęcie posta

JavaScript
// the source file of a video creative
fb.campaign(campaignId).request({ url: `https://graph.facebook.com/${videoId}`, params: { fields: 'source' } })

Łańcuch „reklama → jej zdjęcie" wymaga trzech żądań i nie da się go złożyć inaczej:

JavaScript
const cr = JSON.parse(fb.campaign(campaignId).request({ url: `https://graph.facebook.com/${adId}`, params: { fields: 'creative{id}' } }).text)
const post = JSON.parse(fb.campaign(campaignId).request({ url: `https://graph.facebook.com/${cr.creative.id}`, params: { fields: 'effective_object_story_id' } }).text)
const pic = JSON.parse(fb.campaign(campaignId).request({ url: `https://graph.facebook.com/${post.effective_object_story_id}`, params: { fields: 'full_picture' } }).text)
console.log(pic.full_picture)

Strony, konta Business Manager, posty i komentarze

JavaScript
fb.profile(profileId).request({ url: 'https://graph.facebook.com/me/accounts' }) // fan pages
fb.profile(profileId).request({ url: 'https://graph.facebook.com/me/businesses' }) // business managers
fb.profile(profileId).request({ url: `https://graph.facebook.com/${pageId}/published_posts` }) // page posts

// post comments
const res = fb.profile(profileId).request({
url: `https://graph.facebook.com/${postId}/comments`,
params: { fields: 'id,message,from,created_time', limit: 100 },
})

// delete a comment
fb.profile(profileId).request({ method: 'DELETE', url: `https://graph.facebook.com/${commentId}` })
Qubix potrafi też sam czyścić komentarze

Automatyczne czyszczenie podrzucanych linków, wraz z dziennikiem, jest już wbudowane — sekcja Czyszczenie komentarzy. fb proszę użyć do zbudowania własnej logiki na tym fundamencie: własnych list słów, własnych wyjątków, własnej weryfikacji autorów.

Paginacja

Facebook serwuje długie listy za pomocą kursorów: dopóki odpowiedź niesie paging.next, proszę wziąć paging.cursors.after i powtórzyć żądanie z after w params. Brak paging oznacza koniec.

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)

Jeśli Facebook odpowiada „Please reduce the amount of data you're asking for", to prośba, a nie odmowa: proszę zmniejszyć limit i zacząć od nowa.

Siła tkwi w łączeniu z resztą SDK

Most staje się naprawdę potężny w połączeniu z pozostałymi poleceniami skryptu: withCondition wybiera obiekty według Pana/Pani statystyk, sql pobiera dowolny wycinek z bazy danych, fb sprawdza i zmienia panel reklamowy, ctx.state przechowuje pamięć między uruchomieniami, ctx.fetch wysyła wynik do Pana/Pani komunikatora.

JavaScript
function main() {
// 1. Your own statistics — a direct ClickHouse query: campaign profitability by FUNNEL revenue
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. Live budgets of all candidates — in one exchange with Facebook
const ids = rows.map((r) => r.campaign_id)
if (!ids.length) return
const budgets = fbBatch(fb.campaign(ids[0]), fbReads(ids, 'name,daily_budget'))

for (let i = 0; i < ids.length; i++) {
if (budgets[i].code !== 200) continue
console.log(budgets[i].data.name,
'funnel ROAS:', (rows[i].revenue / rows[i].spend).toFixed(2),
'budget in the cabinet:', budgets[i].data.daily_budget)
}
}

Obiekty można też wybierać bez SQL — za pomocą withCondition: wyrażenia na poziomie SQL po wszystkich metrykach obiektu, z nawiasami, AND/OR/NOT, arytmetyką i porównaniem pól między sobą — spend_24h > 2 * geo_avg_payout, roas_24h < 0.5 * prev_roas_24h. Pełna lista pól znajduje się w zakładce makr w edytorze oraz w Wskaźnikach.

Tak buduje się scenariusze, do których żaden dostawca nigdy by nie doszedł: Pana/Pani lejek decyduje, Pana/Pani profil wykonuje.

Co dalej