Facebook Qubix Bridge
Facebook Qubix Bridge est le SDK qui permet à un script utilisateur de dialoguer directement avec Facebook : le script envoie sa propre requête — vers Graph API ou vers n'importe quelle autre adresse *.facebook.com — et Qubix la transmet via un profil de navigateur que vous avez déjà connecté : avec le jeton d'accès, les cookies, le proxy et la signature de navigateur de ce profil. La réponse de Facebook revient au script telle quelle — jusqu'au corps binaire. Tout ce que le profil est autorisé à faire manuellement dans le gestionnaire de publicités, un script peut désormais le faire selon une planification : lire les paramètres et les statistiques, modifier les budgets, mettre en pause et relancer, télécharger les créatifs, nettoyer les commentaires.
Il n'y a rien à configurer : un profil connecté à Qubix signifie que le pont fonctionne déjà. Ce qu'une requête a le droit de faire est décidé par les droits de ce profil dans Facebook : si le profil ne peut pas modifier une campagne manuellement, Facebook refusera aussi le script, et le script voit ce refus mot pour mot.
C'est un tuyau, et rien qu'un tuyau : vous écrivez l'adresse, la méthode, les paramètres et les en-têtes, tandis que Qubix fournit la clé, les cookies, le proxy et la signature de navigateur. Il n'y a délibérément aucune fonction toute faite de gestion de campagne ici — elle figerait la forme actuelle de Facebook dans le produit et se briserait au premier changement. Reprenez les éléments de haut niveau des exemples et adaptez-les vous-même : le pont ne sait rien de la forme d'une requête, et c'est précisément pour cela qu'il ne devient jamais obsolète.
Le parcours d'une requête
La commande fb est disponible dans les scripts utilisateur — aussi bien selon une planification que via le bouton ▶ Exécuter. Elle n'est pas disponible dans les règles Britva.
Deux façons de choisir le profil exécutant
Chaque requête est envoyée par un profil de navigateur donné — l'exécutant. Vous le nommez vous-même, ou vous laissez Qubix en choisir un.
// 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)— la campagne doit appartenir au périmètre de données de votre script (ce que votre rôle est autorisé à voir). Qubix choisit l'exécutant parmi les profils qui voient cette campagne, en privilégiant les profils du propriétaire de la publicité. Si un candidat échoue au niveau de l'acheminement — un proxy mort, une connexion rompue —, le suivant est essayé, mais seulement pour les lectures : une modification (POST,DELETE) n'est jamais retentée via un autre profil, afin qu'elle ne puisse pas être appliquée deux fois. Dès que Facebook répond — même par un refus —, la recherche s'arrête. La cible porte aussigetProfiles()— la même liste de profils qu'une campagne issue de votre sélection.fb.profile(profileId)— la requête passe exactement par ce profil, sans repli. La cible porte aussi les propres champs du profil (name,tokenAlive,onCheckpoint, …), ce qui permet de distinguer facilement les cibles lorsque vous itérez sur plusieurs d'entre elles.
Quels profils voient un objet
Avant d'envoyer quoi que ce soit, un script peut demander quels profils peuvent agir sur une campagne ou une publicité — avec tout ce que Qubix sait de leur état :
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)
}
Cela renvoie un tableau : une campagne est souvent visible par plusieurs profils, leurs niveaux d'accès dans Facebook diffèrent, et le choix vous appartient. Chaque entrée porte :
| Champ | Signification |
|---|---|
id | identifiant du profil — à passer à fb.profile(...) |
name | nom du profil |
group | groupe du profil |
ownerBuyerId | l'acheteur média propriétaire du profil |
tokenAlive | si le jeton d'accès est actif |
onCheckpoint | si le profil est bloqué sur un checkpoint de sécurité Facebook |
hasProxy | si le profil a un proxy configuré |
Il n'y a délibérément aucun champ « niveau d'accès » : Qubix ne le stocke nulle part. Ce qu'un profil peut faire avec un objet, c'est Facebook lui-même qui y répond — envoyez la requête et lisez la réponse. tokenAlive, onCheckpoint et hasProxy décrivent un état, pas une permission : un profil avec un jeton mort reste dans la liste afin que vous le voyiez et décidiez vous-même. Un objet hors de votre périmètre de données renvoie un tableau vide, pas une erreur.
La requête
request(options) prend un seul objet :
| Option | Signification |
|---|---|
url | l'adresse absolue de la requête : https://graph.facebook.com/${campaignId}, https://graph.facebook.com/act_123/campaigns, https://graph.facebook.com/me/adaccounts. N'importe quel hôte *.facebook.com fonctionne — le pont ajoute les cookies et la clé du profil, et ceux-ci ne partent jamais vers un hôte étranger |
method | GET (par défaut), POST ou DELETE |
params | les paramètres de la requête sous forme d'objet ; POST les porte dans le corps, les autres méthodes — dans l'adresse |
headers | vos propres en-têtes ; ils s'ajoutent aux nôtres et peuvent en écraser n'importe lequel |
timeoutMs | le temps d'attente de la réponse, en millisecondes ; vide — la valeur par défaut de l'opérateur, et vous ne pouvez pas dépasser le plafond |
Le jeton d'accès, les cookies, le proxy et la signature de navigateur sont fournis par le transport propre au profil.
La réponse — et les deux types de refus
La réponse de Facebook arrive telle quelle ; Qubix ne l'analyse pas — c'est à vous de le faire :
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)
| Champ | Signification |
|---|---|
ok | la réponse est arrivée et le code est un succès |
status | le code de réponse HTTP de Facebook |
contentType | le type de contenu de la réponse |
headers | les en-têtes de la réponse — tous sauf Set-Cookie ; c'est là que vivent les éléments absents du corps : les limites de débit restantes, l'identifiant de requête pour un dossier d'assistance auprès de Facebook |
text | le corps tel quel, sous forme de chaîne — JSON.parse(res.text) |
bytes | le même corps en binaire — pour les images et les téléchargements |
profileId | quel profil de navigateur a envoyé la requête |
Il existe deux types de refus, et ils arrivent différemment :
- Qubix n'a pas pu acheminer la requête — un proxy mort, une connexion rompue, une campagne hors de votre périmètre de données — est levé comme une erreur : interceptez-la avec
try/catchsi vous voulez que la boucle continue. Pour les lectures viafb.campaign(...), les profils de repli ont déjà été essayés à ce stade. - Facebook a répondu par un refus — absence de droits, champ erroné, session expirée — est renvoyé comme une réponse normale avec
res.status >= 400et la raison dans le corps :JSON.parse(res.text).errorportemessageetcode, et quanderror_user_titleeterror_user_msgsont présents, il s'agit d'une formulation toute faite destinée à un humain.
Un utilitaire de trois lignes pour ne pas répéter l'analyse dans chaque recette :
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 sert les lectures avec un délai : une valeur lue immédiatement après une modification réussie peut encore être l'ancienne. Ne prenez pas cela pour une écriture échouée. Vérifiez la nouvelle valeur à l'exécution suivante, ou mémorisez ce que vous avez défini dans ctx.state.
Lot : plusieurs requêtes en un seul échange
La file d'attente par profil ne laisse passer qu'une requête par intervalle — une centaine de lectures individuelles ne tiendraient pas dans la limite de temps de l'exécution. Un lot ne paie la file d'attente qu'une seule fois :
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 })— envoie les sous-requêtes en un seul échange ; découpées par tranches de 50 — c'est la limite de Facebook. Un refus du lot entier est levé comme une erreur ; vérifiez le résultat de chaque sous-requête dans soncode, et sa réponse analysée dansdata.fbReads(ids, fields)— construit des sous-requêtes de lecture à partir d'une liste d'identifiants ; vous écrivez lesfields. Une sous-requête peut aussi être construite à la main :{ method: 'GET', relativeUrl: 'act_123/ads?fields=name' }.
Limites et journal
- Le temps d'exécution du script coupe les appels
fbcomme le reste du code — la commande est synchrone, commesqletctx.fetch. - Le délai par appel est défini par l'administrateur dans Système → JavaScript, dans le bloc Facebook (own request via fb.*) : une valeur par défaut et un plafond. Votre
timeoutMss'applique dans la limite de ce plafond. - Une file d'attente par profil. Les requêtes vers un même profil de navigateur sont espacées à l'échelle de toute l'installation — Britva, la collecte de statistiques et les scripts partagent une seule file d'attente, de sorte qu'un script ne peut pas griller un profil à force d'appels fréquents. Besoin de nombreuses lectures — utilisez un lot.
- Chaque requête est enregistrée dans le journal des requêtes sur votre serveur — y compris les lectures : heure, script, profil exécutant, méthode, chemin, résultat et réponse de Facebook. Une plainte du type « le script a ravagé mes campagnes » se règle en une minute grâce au journal. Les jetons d'accès n'atteignent jamais le journal — ils sont retirés des textes enregistrés.
Le catalogue des blocs de construction
Chaque requête ci-dessous a été envoyée par Qubix en fonctionnement réel et a reçu une réponse (vérifié le 27 août 2026) — vous pouvez vous y fier comme socle de base. Tout ce qui va au-delà passe aussi : le pont accepte n'importe quelle adresse *.facebook.com — Graph API et au-delà — et les droits de votre profil décident du reste. L'assistant IA de l'éditeur de script connaît ces blocs et assemblera un script à partir d'une tâche décrite en langage simple.
Des scripts prêts à l'emploi construits à partir de ces blocs se trouvent dans Exemples de scripts.
Qui suis-je sous ce profil
const res = fb.profile(profileId).request({ url: 'https://graph.facebook.com/me', params: { fields: 'id,name,email' } })
Un refus ici signifie que la session du profil est morte. C'est le moyen le moins coûteux de vérifier l'état de la session selon une planification — avant même que les acheteurs média ne s'en aperçoivent.
Comptes publicitaires — argent compris
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',
},
})
L'argent se trouve ici — Graph API n'a pas de point de terminaison « solde » séparé :
| Champ | Ce que c'est |
|---|---|
amount_spent | la dépense totale du compte, en unités mineures de la devise du compte |
adtrust_dsl | la limite de dépense quotidienne du compte |
adspaymentcycle | le seuil de facturation ; le montant se trouve dans .data[0].threshold_amount et se divise par 100 |
account_status | l'état du compte, sous forme de nombre |
disable_reason | la raison de la désactivation, sous forme de nombre |
currency | la devise du compte |
timezone_name | le fuseau horaire du compte ; c'est celui utilisé pour compter « aujourd'hui » |
Cartes de paiement des comptes
La même adresse, un jeu de champs différent — display_string porte le masque de la carte associée :
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}}' },
})
État et budget de la campagne
const res = fb.campaign(campaignId).request({
url: `https://graph.facebook.com/${campaignId}`,
params: { fields: 'id,name,status,effective_status,daily_budget' },
})
daily_budget est renvoyé en unités mineures de la devise du compte, sous forme de chaîne. Si le budget est défini au niveau des ensembles de publicités plutôt qu'au niveau de la campagne, le champ est simplement absent — c'est un état normal, pas une erreur.
Modifier le budget
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
})
En cas de succès, Facebook répond {"success":true}. Le scénario complet avec des seuils de sécurité est la recette « Augmenter le budget des campagnes rentables » dans les exemples.
Paramètres de l'ensemble de publicités : ciblage, budgets, optimisation
Un ensemble de publicités expose ce que nos rapports n'ont pas du tout :
const res = fb.campaign(campaignId).request({
url: `https://graph.facebook.com/${adsetId}`,
params: { fields: 'name,targeting,daily_budget,lifetime_budget,optimization_goal,billing_event' },
})
De la même façon, une seule requête sur une publicité récupère ses objets imbriqués — la campagne, l'ensemble de publicités et le créatif, tous à la fois :
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}' },
})
Mettre en pause et relancer
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' } })
La même forme fonctionne pour une publicité, un ensemble de publicités et une campagne — Facebook les distingue par l'identifiant. Mettre en pause une campagne se propage en cascade : ses publicités indiquent effective_status: 'CAMPAIGN_PAUSED' tandis que leur propre status reste inchangé. Jugez donc si quelque chose tourne réellement d'après effective_status, jamais d'après status.
Mettre en pause et relancer des publicités, ensembles de publicités et campagnes se fait mieux avec les fonctions intégrées .pause() / .activate() : elles passent par la même file d'attente que Britva et conservent la comptabilité complète des pauses. Utilisez fb pour ce dont le SDK n'a pas de commande.
Pourquoi une publicité ne se diffuse pas
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 porte le problème dans les propres mots de Facebook ; adset.end_time repère un ensemble de publicités expiré — la réponse habituelle à « la publicité est active mais rien ne se dépense ».
Statistiques directement depuis le gestionnaire de publicités
Quand vous avez besoin d'une coupe que les rapports Qubix n'ont pas, demandez directement à 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,
},
})
Deux pièges dans lesquels tout le monde tombe :
outbound_clicksetunique_outbound_clicksarrivent sous forme de tableau d'objetsaction_type/value, pas d'un nombre — les autres champs de clics arrivent sous forme de nombre dans une chaîne ;- il n'existe aucun champ
landing_page_viewsde premier niveau — demandezactionset cherchezaction_type: 'landing_page_view'à l'intérieur.
Images des créatifs — jusqu'au corps binaire
const res = fb.campaign(campaignId).request({
url: `https://graph.facebook.com/act_${accountId}/adimages`,
params: { hashes: JSON.stringify([imageHash]), fields: 'url,permalink_url,hash' },
})
Une réponse fb peut aussi être binaire : res.bytes porte le corps tel quel — c'est ainsi que vous récupérez les fichiers eux-mêmes, pas seulement des liens vers eux.
Source vidéo et image de la publication
// the source file of a video creative
fb.campaign(campaignId).request({ url: `https://graph.facebook.com/${videoId}`, params: { fields: 'source' } })
La chaîne « publicité → son image » prend trois requêtes et ne peut pas être assemblée autrement :
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)
Pages, Business Manager, publications et commentaires
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}` })
Le nettoyage automatique des liens indésirables déposés, avec journal, est déjà intégré — la section Nettoyage des commentaires. Utilisez fb pour construire votre propre logique par-dessus : vos propres listes de mots, vos propres exceptions, vos propres vérifications d'auteur.
Pagination
Facebook sert les longues listes avec des curseurs : tant que la réponse porte paging.next, prenez paging.cursors.after et répétez la requête avec after dans params. L'absence de paging signifie la fin.
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)
Si Facebook répond « Please reduce the amount of data you're asking for », il s'agit d'une demande, pas d'un refus : réduisez le limit et recommencez.
Toute la puissance vient de la combinaison avec le reste du SDK
Le pont devient vraiment puissant combiné aux autres commandes du script : withCondition sélectionne des objets d'après vos statistiques, sql extrait n'importe quelle coupe de la base de données, fb vérifie et modifie le gestionnaire de publicités, ctx.state garde la mémoire entre les exécutions, ctx.fetch envoie le résultat à votre messagerie.
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)
}
}
Les objets peuvent aussi être sélectionnés sans SQL — avec withCondition : une expression de niveau SQL portant sur toutes les métriques de l'objet, avec parenthèses, AND/OR/NOT, arithmétique et comparaison champ à champ — spend_24h > 2 * geo_avg_payout, roas_24h < 0.5 * prev_roas_24h. La liste complète des champs se trouve dans l'onglet macros de l'éditeur et dans Métriques.
C'est ainsi que vous construisez des scénarios qu'un éditeur de logiciels ne prendrait jamais la peine de faire : votre entonnoir décide, votre profil exécute.