Zum Hauptinhalt springen

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.

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) — 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ßerdem getProfiles() — 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ß:

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

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:

FeldBedeutung
idKennung des Profils — übergeben Sie sie an fb.profile(...)
nameName des Profils
groupGruppe des Profils
ownerBuyerIdder Buyer, dem das Profil gehört
tokenAliveob das Token noch gültig ist
onCheckpointob das Profil an einem Facebook-Sicherheits-Checkpoint hängt
hasProxyob 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:

OptionBedeutung
urldie 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
methodGET (Standard), POST oder DELETE
paramsAnfrageparameter als Objekt; POST trägt sie im Body, die übrigen Methoden — in der Adresse
headersIhre eigenen Header; sie legen sich über unsere und dürfen jeden davon überschreiben
timeoutMswie 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:

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)
FeldBedeutung
okdie Antwort ist angekommen und der Code ist ein Erfolg
statusder HTTP-Antwortcode von Facebook
contentTypeder Inhaltstyp der Antwort
headersdie 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
textder Body wie er ist, als Zeichenkette — JSON.parse(res.text)
bytesderselbe Body binär — für Bilder und Downloads
profileIdwelches 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/catch ab, wenn die Schleife weiterlaufen soll. Bei Lesevorgängen über fb.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 >= 400 und dem Grund im Body: JSON.parse(res.text).error trägt message und code, und wenn error_user_title und error_user_msg vorhanden sind, ist das eine fertige menschliche Formulierung.

Ein Helfer in drei Zeilen, damit Sie das Auswerten nicht in jedem Rezept wiederholen müssen:

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
}
Nicht sofort nach einer Änderung erneut lesen

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:

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 }) — 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 ihrem code, die geparste Antwort in data.
  • fbReads(ids, fields) — baut Lese-Teilanfragen aus einer Liste von Kennungen; die fields schreiben 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, wie sql und ctx.fetch.
  • Wartezeit pro Aufruf wird vom Administrator in SystemJavaScript, im Block Facebook (own request via fb.*), festgelegt: ein Standardwert und eine Obergrenze. Ihr timeoutMs gilt 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

JavaScript
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

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

Das Geld lebt hier — die Graph API hat keinen eigenen Endpunkt „Kontostand":

FeldWas es ist
amount_spentGesamtausgaben des Kontos, in kleinsten Einheiten der Kontowährung
adtrust_dsldas Tageslimit des Kontos
adspaymentcycledie Abrechnungsschwelle; der Betrag steht in .data[0].threshold_amount und wird durch 100 geteilt
account_statusder Kontostatus, als Zahl
disable_reasonder Sperrgrund, als Zahl
currencydie Kontowährung
timezone_namedie 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:

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

Kampagnenstatus und Budget

JavaScript
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

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

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:

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

Auf dieselbe Weise holt eine einzige Anfrage zu einer Anzeige ihre verschachtelten Objekte — Kampagne, Anzeigengruppe und Creative — auf einmal:

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

Pausieren und starten

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

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.

Für das Pausieren von Anzeigen eignen sich die SDK-Aktionen besser

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

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 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:

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

Zwei Fallen, in die alle tappen:

  • outbound_clicks und unique_outbound_clicks kommen als Array von action_type/value-Objekten, nicht als Zahl — die übrigen Klick-Felder kommen als Zahl in einer Zeichenkette;
  • ein Feld landing_page_views auf oberster Ebene gibt es überhaupt nicht — fragen Sie actions ab und suchen Sie darin nach action_type: 'landing_page_view'.

Creative-Bilder — bis zum binären Inhalt

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

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

JavaScript
// 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:

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)

Seiten, Business Manager, Beiträge und Kommentare

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}` })
Kommentare bereinigen kann Qubix auch von selbst

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.

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)

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.

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

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.

Was kommt als Nächstes