Facebook Qubix Bridge
Facebook Qubix Bridge ist das SDK, mit dem ein Benutzerskript direkt mit Facebook kommuniziert: Das Skript sendet seine eigene Anfrage — an die Graph API oder an eine beliebige andere *.facebook.com-Adresse —, und Qubix leitet sie über ein Browser-Profil weiter, das Sie bereits verbunden haben: mit dessen Zugriffstoken, Cookies, Proxy und Browser-Signatur. Facebooks Antwort kommt unverändert beim Skript an — bis hin zum binären Inhalt. Alles, was das Profil im Ads Manager von Hand tun darf, kann ein Skript jetzt nach Zeitplan erledigen: Einstellungen und Statistiken lesen, Budgets ändern, pausieren und starten, Creatives herunterladen, Kommentare bereinigen.
Es gibt nichts einzurichten: Ist ein Profil mit Qubix verbunden, funktioniert die Brücke bereits. Was eine Anfrage tun darf, entscheiden die Rechte dieses Profils innerhalb von Facebook: Kann das Profil eine Kampagne nicht von Hand bearbeiten, lehnt Facebook auch das Skript ab — und das Skript sieht diese Ablehnung Wort für Wort.
Das ist eine Leitung, und nur eine Leitung: Die Adresse, die Methode, die Parameter und die Header schreiben Sie, während Qubix den Schlüssel, die Cookies, den Proxy und die Browser-Signatur liefert. Fertige Funktionen zur Kampagnenverwaltung gibt es hier bewusst nicht — sie würden die heutige Form von Facebook fest ins Produkt einfrieren und bei dessen erster Änderung zerbrechen. Übernehmen Sie die übergeordneten Bausteine aus den Beispielen und passen Sie sie für sich an: Die Brücke weiß nichts über die Form einer Anfrage — genau deshalb veraltet sie nicht.
Wie eine Anfrage unterwegs ist
Der Befehl fb ist in Benutzerskripten verfügbar — sowohl nach Zeitplan als auch über den Button ▶ Ausführen. In Britva-Regeln gibt es ihn nicht.
Zwei Wege, das ausführende Profil zu wählen
Jede Anfrage wird von irgendeinem Browser-Profil gesendet — dem Ausführenden. Sie benennen es entweder selbst, oder Sie überlassen die Wahl Qubix.
// 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)— die Kampagne muss zum Datenbestand Ihres Skripts gehören (das, was Ihre Rolle sehen darf). Qubix wählt den Ausführenden unter den Profilen, die diese Kampagne sehen, und bevorzugt dabei die Profile des Anzeigen-Eigentümers. Fällt ein Kandidat auf Zustellungsebene aus — toter Proxy, abgebrochene Verbindung —, wird der nächste versucht, aber nur bei Lesevorgängen: Eine Änderung (POST,DELETE) wird nie über ein anderes Profil wiederholt, damit sie nicht doppelt angewendet werden kann. Sobald Facebook antwortet — auch mit einer Ablehnung —, stoppt die Suche. Das Ziel trägt außerdemgetProfiles()— dieselbe Profilliste, die eine Kampagne aus Ihrer Auswahl hat.fb.profile(profileId)— die Anfrage läuft ausschließlich über dieses Profil, ohne Ausweichmöglichkeiten. Das Ziel trägt auch die eigenen Felder des Profils (name,tokenAlive,onCheckpoint, …), sodass sich Ziele leicht unterscheiden lassen, wenn Sie mehrere durchlaufen.
Welche Profile ein Objekt sehen können
Bevor irgendetwas gesendet wird, kann ein Skript abfragen, welche Profile mit einer Kampagne oder Anzeige arbeiten können — mit allem, was Qubix über ihren Zustand weiß:
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)
}
Es wird ein Array zurückgegeben: Eine Kampagne ist oft für mehrere Profile sichtbar, ihre Zugriffsrechte in Facebook unterscheiden sich, und die Wahl liegt bei Ihnen. Jeder Eintrag trägt:
| Feld | Bedeutung |
|---|---|
id | Kennung des Profils — übergeben Sie sie an fb.profile(...) |
name | Name des Profils |
group | Gruppe des Profils |
ownerBuyerId | der Buyer, dem das Profil gehört |
tokenAlive | ob das Token noch gültig ist |
onCheckpoint | ob das Profil an einem Facebook-Sicherheits-Checkpoint hängt |
hasProxy | ob für das Profil ein Proxy konfiguriert ist |
Ein Feld „Zugriffsstufe" gibt es bewusst nicht: Qubix speichert sie nirgends. Was ein Profil mit einem Objekt darf, beantwortet Facebook selbst — schicken Sie die Anfrage und lesen Sie die Antwort. tokenAlive, onCheckpoint und hasProxy sind Zustand, keine Berechtigung: Ein Profil mit totem Token bleibt in der Liste, damit Sie es sehen und selbst entscheiden. Ein Objekt außerhalb Ihres Datenbestands liefert ein leeres Array zurück, keinen Fehler.
Die Anfrage
request(options) nimmt ein Objekt entgegen:
| Option | Bedeutung |
|---|---|
url | die absolute Adresse der Anfrage: https://graph.facebook.com/${campaignId}, https://graph.facebook.com/act_123/campaigns, https://graph.facebook.com/me/adaccounts. Jeder *.facebook.com-Host funktioniert — die Brücke hängt die Cookies und den Schlüssel des Profils an, und die gehen nie an einen fremden Host |
method | GET (Standard), POST oder DELETE |
params | Anfrageparameter als Objekt; POST trägt sie im Body, die übrigen Methoden — in der Adresse |
headers | Ihre eigenen Header; sie legen sich über unsere und dürfen jeden davon überschreiben |
timeoutMs | wie lange auf die Antwort gewartet wird, in Millisekunden; leer — der Standardwert des Betreibers, und über dessen Obergrenze kommen Sie nicht hinaus |
Zugriffstoken, Cookies, Proxy und Browser-Signatur liefert der Transport des Profils selbst.
Die Antwort — und die zwei Arten der Ablehnung
Facebooks Antwort kommt wie sie ist an; Qubix wertet sie nicht aus — das tun Sie:
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)
| Feld | Bedeutung |
|---|---|
ok | die Antwort ist angekommen und der Code ist ein Erfolg |
status | der HTTP-Antwortcode von Facebook |
contentType | der Inhaltstyp der Antwort |
headers | die Antwort-Header — alle außer Set-Cookie; hier lebt, was im Body fehlt: die verbleibenden Ratenlimits, die Kennung der Anfrage für einen Support-Fall bei Facebook |
text | der Body wie er ist, als Zeichenkette — JSON.parse(res.text) |
bytes | derselbe Body binär — für Bilder und Downloads |
profileId | welches Browser-Profil die Anfrage gesendet hat |
Es gibt zwei Arten der Ablehnung, und sie kommen unterschiedlich an:
- Qubix konnte nicht zustellen — toter Proxy, abgebrochene Verbindung, Kampagne außerhalb Ihres Datenbestands — wird als Fehler geworfen: Fangen Sie ihn mit
try/catchab, wenn die Schleife weiterlaufen soll. Bei Lesevorgängen überfb.campaign(...)sind die Ausweichprofile zu diesem Zeitpunkt bereits durchprobiert. - Facebook hat mit einer Ablehnung geantwortet — keine Rechte, ein falsches Feld, eine abgelaufene Sitzung — kommt als normale Antwort zurück mit
res.status >= 400und dem Grund im Body:JSON.parse(res.text).errorträgtmessageundcode, und wennerror_user_titleunderror_user_msgvorhanden sind, ist das eine fertige menschliche Formulierung.
Ein Helfer in drei Zeilen, damit Sie das Auswerten nicht in jedem Rezept wiederholen müssen:
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 liefert Lesevorgänge mit Verzögerung: Ein Wert, der unmittelbar nach einer erfolgreichen Änderung gelesen wird, kann noch der alte sein. Werten Sie das nicht als fehlgeschlagenes Schreiben. Prüfen Sie den neuen Wert im nächsten Lauf, oder merken Sie sich, was Sie gesetzt haben, in ctx.state.
Stapel: viele Anfragen in einem Austausch
Die Warteschlange pro Profil lässt pro Intervall eine Anfrage durch — hundert einzelne Lesevorgänge passen nicht in das Zeitlimit des Laufs. Ein Stapel bezahlt die Warteschlange nur einmal:
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 })— sendet die Teilanfragen in einem Austausch; aufgeteilt in Fünfzigerblöcke — das ist Facebooks Grenze. Eine Ablehnung des gesamten Stapels wird als Fehler geworfen; das Ergebnis jeder Teilanfrage sehen Sie in ihremcode, die geparste Antwort indata.fbReads(ids, fields)— baut Lese-Teilanfragen aus einer Liste von Kennungen; diefieldsschreiben Sie selbst. Eine Teilanfrage lässt sich auch von Hand bauen:{ method: 'GET', relativeUrl: 'act_123/ads?fields=name' }.
Limits und das Protokoll
- Skript-Laufzeit kappt
fb-Aufrufe genau wie den übrigen Code — der Befehl ist synchron, wiesqlundctx.fetch. - Wartezeit pro Aufruf wird vom Administrator in System → JavaScript, im Block Facebook (own request via fb.*), festgelegt: ein Standardwert und eine Obergrenze. Ihr
timeoutMsgilt innerhalb dieser Obergrenze. - Eine Warteschlange pro Profil. Anfragen an ein Browser-Profil werden serverweit zeitlich gestreckt — Britva, die Statistikerfassung und Skripte teilen sich eine Warteschlange, sodass ein Skript ein Profil nicht mit häufigen Aufrufen verbrennen kann. Brauchen Sie viele Lesevorgänge — nutzen Sie einen Stapel.
- Jede Anfrage wird protokolliert — im Anfrage-Protokoll auf Ihrem Server, auch Lesevorgänge: Zeit, Skript, ausführendes Profil, Methode, Pfad, Ergebnis und Facebooks Antwort. Eine Beschwerde wie „das Skript hat meine Kampagnen zerstört" lässt sich anhand des Protokolls in einer Minute klären. Zugriffstoken gelangen niemals ins Protokoll — sie werden aus den gespeicherten Texten entfernt.
Katalog der Bausteine
Jede Anfrage unten hat Qubix im laufenden Betrieb tatsächlich gesendet und eine Antwort erhalten (geprüft am 27. August 2026) — Sie können sich auf sie als Grundbestand verlassen. Alles darüber hinaus funktioniert ebenso: Die Brücke akzeptiert jede *.facebook.com-Adresse — Graph API und mehr —, und den Rest entscheiden die Rechte Ihres Profils. Der KI-Assistent im Skript-Editor kennt diese Bausteine und setzt aus einer in einfachen Worten beschriebenen Aufgabe ein Skript zusammen.
Fertige Skripte, die aus diesen Bausteinen gebaut sind, finden Sie unter Skript-Beispiele.
Wer bin ich unter diesem Profil
const res = fb.profile(profileId).request({ url: 'https://graph.facebook.com/me', params: { fields: 'id,name,email' } })
Eine Ablehnung hier bedeutet, dass die Sitzung des Profils tot ist. Das ist der günstigste Weg, die Gesundheit der Sitzung nach Zeitplan zu prüfen — früher, als es der Media Buyer bemerkt.
Werbekonten — auch die Finanzen
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',
},
})
Das Geld lebt hier — die Graph API hat keinen eigenen Endpunkt „Kontostand":
| Feld | Was es ist |
|---|---|
amount_spent | Gesamtausgaben des Kontos, in kleinsten Einheiten der Kontowährung |
adtrust_dsl | das Tageslimit des Kontos |
adspaymentcycle | die Abrechnungsschwelle; der Betrag steht in .data[0].threshold_amount und wird durch 100 geteilt |
account_status | der Kontostatus, als Zahl |
disable_reason | der Sperrgrund, als Zahl |
currency | die Kontowährung |
timezone_name | die Zeitzone des Kontos; „heute" wird danach berechnet |
Zahlungskarten der Konten
Dieselbe Adresse, ein anderer Satz Felder — display_string trägt die Maske der hinterlegten Karte:
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}}' },
})
Kampagnenstatus und Budget
const res = fb.campaign(campaignId).request({
url: `https://graph.facebook.com/${campaignId}`,
params: { fields: 'id,name,status,effective_status,daily_budget' },
})
daily_budget wird in kleinsten Einheiten der Kontowährung zurückgegeben, als Zeichenkette. Ist das Budget auf den Anzeigengruppen statt auf der Kampagne festgelegt, fehlt das Feld einfach — das ist ein normaler Zustand, kein Fehler.
Das Budget ändern
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
})
Bei Erfolg antwortet Facebook mit {"success":true}. Das vollständige Szenario mit Sicherheitsschwellen ist das Rezept „Budget rentabler Kampagnen anheben" in den Beispielen.
Anzeigengruppen-Einstellungen: Targeting, Budgets, Optimierung
Eine Anzeigengruppe zeigt, was in unseren Berichten gar nicht vorkommt:
const res = fb.campaign(campaignId).request({
url: `https://graph.facebook.com/${adsetId}`,
params: { fields: 'name,targeting,daily_budget,lifetime_budget,optimization_goal,billing_event' },
})
Auf dieselbe Weise holt eine einzige Anfrage zu einer Anzeige ihre verschachtelten Objekte — Kampagne, Anzeigengruppe und Creative — auf einmal:
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}' },
})
Pausieren und starten
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' } })
Dieselbe Form funktioniert für eine Anzeige, eine Anzeigengruppe und eine Kampagne — Facebook unterscheidet sie anhand der Kennung. Das Pausieren einer Kampagne wirkt kaskadierend: Ihre Anzeigen melden effective_status: 'CAMPAIGN_PAUSED', während ihr eigener status unverändert bleibt. Beurteilen Sie also, ob wirklich etwas läuft, anhand von effective_status — niemals anhand von status.
Anzeigen, Anzeigengruppen und Kampagnen pausieren und wieder aktivieren Sie besser mit den eingebauten .pause() / .activate(): Sie laufen über dieselbe Warteschlange wie Britva und führen die vollständige Pausen-Buchführung. Nutzen Sie fb für das, wofür das SDK keinen eigenen Befehl hat.
Warum eine Anzeige nicht ausliefert
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 trägt das Problem in Facebooks eigenen Worten; adset.end_time erfasst eine abgelaufene Anzeigengruppe — die übliche Antwort auf „die Anzeige ist aktiv, aber es wird nichts ausgegeben".
Statistiken direkt aus dem Werbekonto
Brauchen Sie einen Ausschnitt, den die Qubix-Berichte nicht haben, fragen Sie Facebook direkt:
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,
},
})
Zwei Fallen, in die alle tappen:
outbound_clicksundunique_outbound_clickskommen als Array vonaction_type/value-Objekten, nicht als Zahl — die übrigen Klick-Felder kommen als Zahl in einer Zeichenkette;- ein Feld
landing_page_viewsauf oberster Ebene gibt es überhaupt nicht — fragen Sieactionsab und suchen Sie darin nachaction_type: 'landing_page_view'.
Creative-Bilder — bis zum binären Inhalt
const res = fb.campaign(campaignId).request({
url: `https://graph.facebook.com/act_${accountId}/adimages`,
params: { hashes: JSON.stringify([imageHash]), fields: 'url,permalink_url,hash' },
})
Eine fb-Antwort kann auch binär sein: res.bytes trägt den Inhalt wie er ist — so holen Sie die Dateien selbst, nicht nur Links zu ihnen.
Videoquelle und das Beitragsbild
// the source file of a video creative
fb.campaign(campaignId).request({ url: `https://graph.facebook.com/${videoId}`, params: { fields: 'source' } })
Die Kette „Anzeige → ihr Bild" braucht drei Anfragen und lässt sich nicht anders zusammensetzen:
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)
Seiten, Business Manager, Beiträge und Kommentare
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}` })
Die automatische Bereinigung untergeschobener Links, mit Protokoll, ist bereits eingebaut — Abschnitt Kommentar-Bereinigung. Nutzen Sie fb, um Ihre eigene Logik darauf aufzubauen: eigene Wortlisten, eigene Ausnahmen, eigene Autorenprüfungen.
Paginierung
Facebook liefert lange Listen mit Cursorn: Solange die Antwort paging.next enthält, nehmen Sie paging.cursors.after und wiederholen die Anfrage mit after in params. Kein paging bedeutet das Ende.
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)
Antwortet Facebook mit „Please reduce the amount of data you're asking for", ist das eine Bitte, keine Ablehnung: verringern Sie das limit und beginnen Sie neu.
Die Stärke liegt in der Kombination mit dem Rest des SDK
Die Brücke wird zusammen mit den übrigen Befehlen des Skripts erst richtig stark: withCondition wählt Objekte anhand Ihrer Statistik aus, sql holt jeden beliebigen Ausschnitt aus der Datenbank, fb prüft und ändert das Konto, ctx.state bewahrt das Gedächtnis zwischen den Läufen, ctx.fetch schickt das Ergebnis an Ihren Messenger.
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)
}
}
Objekte lassen sich auch ohne SQL auswählen — mit withCondition: ein Ausdruck auf SQL-Niveau über alle Kennzahlen des Objekts, mit Klammern, AND/OR/NOT, Arithmetik und dem Vergleich von Feldern untereinander — spend_24h > 2 * geo_avg_payout, roas_24h < 0.5 * prev_roas_24h. Die vollständige Feldliste finden Sie im Makro-Tab des Editors und unter Metriken.
So bauen Sie Szenarien, für die ein Anbieter nie Zeit gefunden hätte: Ihr Funnel entscheidet, Ihr Profil führt aus.