Pular para o conteúdo principal

Facebook Qubix Bridge

Facebook Qubix Bridge é o SDK que permite que um script do usuário se comunique diretamente com o Facebook: o script envia sua própria requisição — para a Graph API ou para qualquer outro endereço *.facebook.com — e o Qubix a transporta através de um perfil de navegador que você já conectou: com o token de acesso, os cookies, o proxy e a assinatura de navegador desse perfil. A resposta do Facebook volta ao script exatamente como veio — até o corpo binário. Tudo o que o perfil pode fazer manualmente no painel de anúncios, um script agora pode fazer de forma programada: ler configurações e estatísticas, alterar orçamentos, pausar e ativar, baixar criativos, limpar comentários.

Não há nada para configurar: um perfil conectado ao Qubix significa que a ponte já funciona. O que uma requisição pode fazer é decidido pelas permissões desse perfil dentro do Facebook: se o perfil não pode editar uma campanha manualmente, o Facebook também recusará o script, e o script vê essa recusa palavra por palavra.

Isto é um canal, e apenas um canal: você escreve o endereço, o método, os parâmetros e os cabeçalhos, enquanto o Qubix fornece a chave, os cookies, o proxy e a assinatura de navegador. Propositalmente, não há aqui funções prontas de gerenciamento de campanhas — elas engessariam a forma atual do Facebook dentro do produto e quebrariam na primeira mudança. Pegue os blocos de nível mais alto dos exemplos e adapte-os para o seu caso: a ponte não sabe nada sobre o formato de uma requisição, e é exatamente por isso que ela não fica obsoleta.

Como uma requisição viaja

O comando fb está disponível em scripts do usuário — tanto no agendamento quanto pelo botão ▶ Executar. Ele não está disponível nas regras da Britva.

Duas formas de escolher o perfil executor

Toda requisição é enviada por algum perfil de navegador — o executor. Você mesmo o nomeia ou deixa o Qubix escolher um.

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) — a campanha precisa pertencer ao conjunto de dados do seu script (o que o seu papel tem permissão para ver). O Qubix escolhe o executor entre os perfis que veem essa campanha, preferindo os perfis do dono do anúncio. Se um candidato falhar no nível de entrega — um proxy morto, uma conexão quebrada —, o próximo é tentado, mas somente para leituras: uma alteração (POST, DELETE) nunca é reenviada por outro perfil, para que não seja aplicada duas vezes. Assim que o Facebook responde — mesmo com uma recusa —, a busca para. O alvo também carrega getProfiles() — a mesma lista de perfis que uma campanha da sua seleção tem.
  • fb.profile(profileId) — a requisição passa exatamente por esse perfil, sem alternativas. O alvo também carrega os próprios campos do perfil (name, tokenAlive, onCheckpoint, …), o que facilita distinguir os alvos ao iterar sobre vários.

Quais perfis veem um objeto

Antes de enviar qualquer coisa, um script pode perguntar quais perfis podem agir sobre uma campanha ou um anúncio — com tudo o que o Qubix sabe sobre o estado deles:

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

Isso retorna um array: uma campanha costuma ficar visível para vários perfis, os níveis de acesso deles no Facebook diferem, e a escolha é sua. Cada item carrega:

CampoSignificado
ididentificador do perfil — passe-o para fb.profile(...)
namenome do perfil
groupgrupo do perfil
ownerBuyerIdo buyer dono do perfil
tokenAlivese o token de acesso é válido
onCheckpointse o perfil está preso em um checkpoint de segurança do Facebook
hasProxyse o perfil tem um proxy configurado

Propositalmente não há um campo de «nível de acesso»: o Qubix não o armazena em lugar nenhum. O que um perfil pode fazer com um objeto é respondido pelo próprio Facebook — envie a requisição e leia a resposta. tokenAlive, onCheckpoint e hasProxy são estado, não permissão: um perfil com token morto permanece na lista para que você o veja e decida por conta própria. Um objeto fora do seu conjunto de dados retorna um array vazio, não um erro.

A requisição

request(options) recebe um objeto:

OpçãoSignificado
urlo endereço absoluto da requisição: https://graph.facebook.com/${campaignId}, https://graph.facebook.com/act_123/campaigns, https://graph.facebook.com/me/adaccounts. Qualquer host *.facebook.com funciona — a ponte anexa os cookies e a chave do perfil, e eles nunca vão para um host externo
methodGET (padrão), POST ou DELETE
paramsparâmetros da requisição como um objeto; POST os carrega no corpo, os demais métodos — no endereço
headersseus próprios cabeçalhos; eles se sobrepõem aos nossos e podem substituir qualquer um
timeoutMsquanto tempo esperar pela resposta, em milissegundos; vazio — o padrão do operador, e você não pode ultrapassar o teto

O token de acesso, os cookies, o proxy e a assinatura de navegador são fornecidos pelo próprio transporte do perfil.

A resposta — e os dois tipos de recusa

A resposta do Facebook chega como veio; o Qubix não a interpreta — você faz isso:

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)
CampoSignificado
oka resposta chegou e o código é de sucesso
statuso código de resposta HTTP do Facebook
contentTypeo tipo de conteúdo da resposta
headersos cabeçalhos da resposta — todos, exceto Set-Cookie; é aqui que ficam as coisas ausentes do corpo: os limites de taxa restantes, o identificador da requisição para um chamado de suporte junto ao Facebook
texto corpo como veio, como uma string — JSON.parse(res.text)
byteso mesmo corpo em binário — para imagens e downloads
profileIdqual perfil de navegador enviou a requisição

Há dois tipos de recusa, e eles chegam de formas diferentes:

  • O Qubix não conseguiu entregar — um proxy morto, uma conexão quebrada, uma campanha fora do seu conjunto de dados — é lançado como um erro: capture-o com try/catch se quiser que o loop continue. Para leituras via fb.campaign(...), os perfis alternativos já foram tentados até este ponto.
  • O Facebook respondeu com uma recusa — sem permissões, um campo errado, uma sessão expirada — é retornado como uma resposta normal, com res.status >= 400 e o motivo no corpo: JSON.parse(res.text).error carrega message e code, e, quando error_user_title e error_user_msg estão presentes, essa já é uma mensagem pronta, em linguagem natural, para mostrar ao usuário.

Um auxiliar de três linhas para você não repetir a interpretação em cada receita:

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
}
Não releia logo depois de uma alteração

O Facebook serve as leituras com atraso: um valor lido logo após uma alteração bem-sucedida ainda pode ser o antigo. Não trate isso como uma gravação falha. Verifique o novo valor na próxima execução, ou lembre o que você definiu em ctx.state.

Lote: várias requisições em uma única troca

A fila por perfil deixa passar uma requisição por intervalo — cem leituras isoladas não caberiam no limite de tempo da execução. Um lote paga a fila uma única vez:

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 }) — envia as subrequisições em uma única troca; divididas em blocos de 50 — esse é o limite do Facebook. Uma recusa do lote inteiro é lançada; verifique o resultado de cada subrequisição em seu code, e sua resposta interpretada em data.
  • fbReads(ids, fields) — monta subrequisições de leitura a partir de uma lista de identificadores; você escreve os fields. Uma subrequisição também pode ser montada manualmente: { method: 'GET', relativeUrl: 'act_123/ads?fields=name' }.

Limites e o registro

  • O tempo de execução do script corta as chamadas fb como o resto do código — o comando é síncrono, assim como sql e ctx.fetch.
  • A espera por chamada é definida pelo administrador em SistemaJavaScript, no bloco Facebook (own request via fb.*): um padrão e um teto. Seu timeoutMs se aplica dentro do teto.
  • Uma fila por perfil. As requisições a um mesmo perfil de navegador são espaçadas para toda a instalação — a Britva, a coleta de estatísticas e os scripts compartilham uma única fila, de modo que um script não pode sobrecarregar um perfil com chamadas frequentes. Precisa de muitas leituras — use um lote.
  • Toda requisição é registrada no registro de requisições do seu servidor — inclusive as leituras: horário, script, perfil executor, método, caminho, resultado e a resposta do Facebook. Uma reclamação como «o script bagunçou minhas campanhas» leva um minuto para ser esclarecida pelo registro. Tokens de acesso nunca chegam ao registro — eles são removidos dos textos armazenados.

O catálogo de blocos de construção

Toda requisição abaixo já foi enviada pelo Qubix em operação real e respondida (verificado em 27 de agosto de 2026) — você pode confiar nelas como o conjunto base. Qualquer coisa além disso também passa: a ponte aceita qualquer endereço *.facebook.com — Graph API e além — e as permissões do seu perfil decidem o resto. O assistente de IA no editor de scripts conhece esses blocos e vai montar um script a partir de uma tarefa descrita em palavras simples.

Scripts prontos montados a partir desses blocos estão em Exemplos de scripts.

Quem sou eu com este perfil

JavaScript
const res = fb.profile(profileId).request({ url: 'https://graph.facebook.com/me', params: { fields: 'id,name,email' } })

Uma recusa aqui significa que a sessão do perfil está morta. Essa é a forma mais barata de verificar a saúde da sessão em um agendamento — antes mesmo que o buyer perceba.

Contas de anúncios — incluindo o dinheiro

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

O dinheiro fica aqui — a Graph API não tem um endpoint separado de «saldo»:

CampoO que é
amount_spentgasto total da conta, em unidades menores da moeda da conta
adtrust_dslo limite de gasto diário da conta
adspaymentcycleo limiar de cobrança; o valor fica em .data[0].threshold_amount e é dividido por 100
account_statuso estado da conta, como número
disable_reasono motivo pelo qual foi desativada, como número
currencya moeda da conta
timezone_nameo fuso horário da conta; é nele que o «hoje» é contado

Cartões de pagamento das contas

O mesmo endereço, um conjunto diferente de campos — display_string carrega a máscara do cartão vinculado:

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

Status e orçamento da campanha

JavaScript
const res = fb.campaign(campaignId).request({
url: `https://graph.facebook.com/${campaignId}`,
params: { fields: 'id,name,status,effective_status,daily_budget' },
})

daily_budget é retornado em unidades menores da moeda da conta, como string. Se o orçamento estiver definido nos conjuntos de anúncios em vez de na campanha, o campo simplesmente fica ausente — isso é um estado normal, não um erro.

Alterando o orçamento

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

Em caso de sucesso, o Facebook responde {"success":true}. O cenário completo com limiares de segurança é a receita «Aumentando o orçamento de campanhas lucrativas» nos exemplos.

Configurações do conjunto de anúncios: segmentação, orçamentos, otimização

Um conjunto de anúncios expõe o que os nossos relatórios não têm de forma alguma:

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

Da mesma forma, uma única requisição sobre um anúncio traz seus objetos aninhados — a campanha, o conjunto de anúncios e o criativo de uma só vez:

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

Pausando e ativando

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

A mesma forma funciona para um anúncio, um conjunto de anúncios e uma campanha — o Facebook os diferencia pelo identificador. Pausar uma campanha se propaga em cascata: seus anúncios relatam effective_status: 'CAMPAIGN_PAUSED', enquanto o próprio status deles permanece inalterado. Portanto, avalie se algo está realmente em execução pelo effective_status, nunca pelo status.

Prefira as ações do SDK para pausar anúncios

Pausar e reativar anúncios, conjuntos de anúncios e campanhas é melhor feito com os comandos nativos .pause() / .activate(): eles passam pela mesma fila que a Britva e mantêm todo o registro de pausas. Use o fb para o que o SDK não tem comando.

Por que um anúncio não está entregando

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 traz o problema nas próprias palavras do Facebook; adset.end_time identifica um conjunto de anúncios expirado — a resposta mais comum para «o anúncio está ativo, mas nada gasta».

Estatísticas direto do painel

Quando você precisa de um recorte que os relatórios do Qubix não têm, pergunte ao próprio 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,
},
})

Duas armadilhas em que todo mundo cai:

  • outbound_clicks e unique_outbound_clicks chegam como um array de objetos action_type/value, não como número — os demais campos de cliques chegam como número dentro de uma string;
  • não existe de forma alguma um campo landing_page_views no nível superior — peça actions e procure por action_type: 'landing_page_view' dentro dele.

Imagens de criativos — até o corpo binário

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

Uma resposta do fb também pode ser binária: res.bytes carrega o corpo como veio — é assim que você pega os próprios arquivos, não apenas links para eles.

Origem do vídeo e a imagem da publicação

JavaScript
// the source file of a video creative
fb.campaign(campaignId).request({ url: `https://graph.facebook.com/${videoId}`, params: { fields: 'source' } })

A cadeia «anúncio → sua imagem» leva três requisições e não pode ser montada de outra forma:

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)

Páginas, gerenciadores de negócios, publicações e comentários

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}` })
O Qubix também sabe limpar comentários sozinho

A remoção automática de links plantados, com registro, já vem integrada — veja a seção Limpeza de comentários. Use o fb para montar sua própria lógica em cima disso: suas próprias listas de palavras, suas próprias exceções, suas próprias verificações de autor.

Paginação

O Facebook serve listas longas com cursores: enquanto a resposta carregar paging.next, pegue paging.cursors.after e repita a requisição com after em params. A ausência de paging significa o fim.

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)

Se o Facebook responder «Please reduce the amount of data you're asking for», isso é um pedido, não uma recusa: reduza o limit e comece de novo.

O poder está em combinar com o resto do SDK

A ponte se torna verdadeiramente poderosa junto com os outros comandos do script: withCondition seleciona objetos pelas suas estatísticas, sql traz qualquer recorte do banco de dados, fb verifica e altera o painel, ctx.state mantém memória entre execuções, ctx.fetch envia o resultado para o seu mensageiro.

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

Objetos também podem ser selecionados sem SQL — com withCondition: uma expressão de nível SQL sobre todas as métricas do objeto, com parênteses, AND/OR/NOT, aritmética e comparação campo a campo — spend_24h > 2 * geo_avg_payout, roas_24h < 0.5 * prev_roas_24h. A lista completa de campos está na aba de macros do editor e em Métricas.

É assim que você constrói cenários que um fornecedor pronto nunca chegaria a oferecer: seu funil decide, seu perfil executa.

O que vem a seguir