Aller au contenu principal

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.

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

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

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 :

ChampSignification
ididentifiant du profil — à passer à fb.profile(...)
namenom du profil
groupgroupe du profil
ownerBuyerIdl'acheteur média propriétaire du profil
tokenAlivesi le jeton d'accès est actif
onCheckpointsi le profil est bloqué sur un checkpoint de sécurité Facebook
hasProxysi 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 :

OptionSignification
urll'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
methodGET (par défaut), POST ou DELETE
paramsles paramètres de la requête sous forme d'objet ; POST les porte dans le corps, les autres méthodes — dans l'adresse
headersvos propres en-têtes ; ils s'ajoutent aux nôtres et peuvent en écraser n'importe lequel
timeoutMsle 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 :

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)
ChampSignification
okla réponse est arrivée et le code est un succès
statusle code de réponse HTTP de Facebook
contentTypele type de contenu de la réponse
headersles 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
textle corps tel quel, sous forme de chaîne — JSON.parse(res.text)
bytesle même corps en binaire — pour les images et les téléchargements
profileIdquel 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/catch si vous voulez que la boucle continue. Pour les lectures via fb.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 >= 400 et la raison dans le corps : JSON.parse(res.text).error porte message et code, et quand error_user_title et error_user_msg sont 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 :

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
}
Ne relisez pas juste après une modification

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 :

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 }) — 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 son code, et sa réponse analysée dans data.
  • fbReads(ids, fields) — construit des sous-requêtes de lecture à partir d'une liste d'identifiants ; vous écrivez les fields. 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 fb comme le reste du code — la commande est synchrone, comme sql et ctx.fetch.
  • Le délai par appel est défini par l'administrateur dans SystèmeJavaScript, dans le bloc Facebook (own request via fb.*) : une valeur par défaut et un plafond. Votre timeoutMs s'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

JavaScript
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

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

L'argent se trouve ici — Graph API n'a pas de point de terminaison « solde » séparé :

ChampCe que c'est
amount_spentla dépense totale du compte, en unités mineures de la devise du compte
adtrust_dslla limite de dépense quotidienne du compte
adspaymentcyclele seuil de facturation ; le montant se trouve dans .data[0].threshold_amount et se divise par 100
account_statusl'état du compte, sous forme de nombre
disable_reasonla raison de la désactivation, sous forme de nombre
currencyla devise du compte
timezone_namele 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 :

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

État et budget de la campagne

JavaScript
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

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

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 :

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

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 :

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

Mettre en pause et relancer

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

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.

Préférez les actions du SDK pour mettre en pause les publicités

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

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

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

Deux pièges dans lesquels tout le monde tombe :

  • outbound_clicks et unique_outbound_clicks arrivent sous forme de tableau d'objets action_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_views de premier niveau — demandez actions et cherchez action_type: 'landing_page_view' à l'intérieur.

Images des créatifs — jusqu'au corps binaire

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

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

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

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)

Pages, Business Manager, publications et commentaires

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 sait aussi nettoyer les commentaires tout seul

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.

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)

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.

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

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.

Et ensuite