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.
// 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:
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:
| Pole | Znaczenie |
|---|---|
id | identyfikator profilu — proszę przekazać go do fb.profile(...) |
name | nazwa profilu |
group | grupa profilu |
ownerBuyerId | buyer, który jest właścicielem profilu |
tokenAlive | czy token dostępu jest aktywny |
onCheckpoint | czy profil utknął na weryfikacji bezpieczeństwa Facebooka (checkpoint) |
hasProxy | czy 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:
| Opcja | Znaczenie |
|---|---|
url | bezwzglę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 |
method | GET (domyślnie), POST lub DELETE |
params | parametry żądania jako obiekt; POST przenosi je w treści, pozostałe metody — w adresie |
headers | własne nagłówki; nakładają się na nasze i mogą nadpisać każdy z nich |
timeoutMs | jak 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:
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)
| Pole | Znaczenie |
|---|---|
ok | odpowiedź dotarła, a kod jest sukcesem |
status | kod odpowiedzi HTTP od Facebooka |
contentType | typ zawartości odpowiedzi |
headers | nagłó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 |
text | treść bez zmian, jako string — JSON.parse(res.text) |
bytes | ta sama treść w postaci binarnej — do obrazów i pobierania plików |
profileId | któ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 przezfb.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 >= 400i przyczyną w treści:JSON.parse(res.text).errorniesiemessageicode, a gdy obecne sąerror_user_titleierror_user_msg— jest to gotowe sformułowanie dla człowieka.
Trzylinijkowa funkcja pomocnicza, aby nie powtarzać parsowania w każdym przepisie:
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
}
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:
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 jegocode, a jego sparsowaną odpowiedź — wdata.fbReads(ids, fields)— buduje podżądania odczytu z listy identyfikatorów;fieldsproszę 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
fbtak samo jak resztę kodu — polecenie jest synchroniczne, tak samo jaksqlictx.fetch. - Czas oczekiwania na wywołanie ustawia administrator w System → JavaScript, w bloku Facebook (own request via fb.*): wartość domyślna i pułap. Pana/Pani
timeoutMsdział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
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
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":
| Pole | Co to jest |
|---|---|
amount_spent | łączne wydatki konta, w jednostkach pomocniczych waluty konta |
adtrust_dsl | dzienny limit wydatków konta |
adspaymentcycle | próg rozliczeniowy; kwota znajduje się w .data[0].threshold_amount i jest dzielona przez 100 |
account_status | stan konta, jako liczba |
disable_reason | powód wyłączenia, jako liczba |
currency | waluta konta |
timezone_name | strefa 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:
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
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
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:
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:
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
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.
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
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:
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_clicksiunique_outbound_clicksprzychodzą jako tablica obiektówaction_type/value, a nie jako liczba — pozostałe pola kliknięć przychodzą jako liczba w stringu;- pola
landing_page_viewsna najwyższym poziomie w ogóle nie ma — proszę zażądaćactionsi poszukać w nimaction_type: 'landing_page_view'.
Obrazy kreacji — łącznie z treścią binarną
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
// 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:
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
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}` })
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.
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.
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.