Saltar al contenido principal

Facebook Qubix Bridge

Facebook Qubix Bridge es el SDK que permite que un script de usuario hable directamente con Facebook: el script envía su propia solicitud — a la Graph API o a cualquier otra dirección *.facebook.com — y Qubix la transporta a través de un perfil de navegador que usted ya ha conectado: con el token de acceso, las cookies, el proxy y la firma del navegador de ese perfil. La respuesta de Facebook vuelve al script tal cual — hasta el cuerpo binario. Cualquier cosa que el perfil pueda hacer a mano en la cuenta publicitaria, un script ahora puede hacerla según una programación: leer ajustes y estadísticas, cambiar presupuestos, pausar e iniciar, descargar creativos, limpiar comentarios.

No hay nada que configurar: si un perfil está conectado a Qubix, el puente ya funciona. Lo que una solicitud puede hacer lo deciden los derechos de ese perfil dentro de Facebook: si el perfil no puede editar una campaña a mano, Facebook también rechazará el script, y el script ve ese rechazo palabra por palabra.

Esto es un conducto, y solo un conducto: usted escribe la dirección, el método, los parámetros y las cabeceras, mientras que Qubix aporta la clave, las cookies, el proxy y la firma del navegador. Deliberadamente no hay aquí funciones de gestión de campañas ya hechas — congelarían la forma actual de Facebook dentro del producto y se romperían con su primer cambio. Tome los fragmentos de alto nivel de los ejemplos y adáptelos usted mismo: el puente no sabe nada sobre la forma de una solicitud, y precisamente por eso no queda obsoleto.

Cómo viaja una solicitud

El comando fb está disponible en los scripts de usuario — tanto según una programación como mediante el botón ▶ Ejecutar. No está disponible en las reglas de Britva.

Dos maneras de elegir el perfil ejecutor

Toda solicitud la envía algún perfil de navegador — el ejecutor. Usted puede indicarlo usted mismo o dejar que Qubix elija uno.

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 campaña debe pertenecer al conjunto de datos de su script (lo que su rol tiene permitido ver). Qubix elige el ejecutor entre los perfiles que ven esta campaña, prefiriendo los perfiles del propietario del anuncio. Si un candidato falla al nivel de entrega — un proxy caído, una conexión rota — se prueba el siguiente, pero solo para lecturas: un cambio (POST, DELETE) nunca se reintenta a través de otro perfil, para que no pueda aplicarse dos veces. En cuanto Facebook responde — incluso con un rechazo — la búsqueda se detiene. El objetivo también lleva getProfiles() — la misma lista de perfiles que tiene una campaña de su selección.
  • fb.profile(profileId) — la solicitud pasa exactamente por este perfil, sin alternativas. El objetivo también lleva los propios campos del perfil (name, tokenAlive, onCheckpoint, …), de modo que los objetivos se distinguen fácilmente cuando itera sobre varios.

Qué perfiles ven un objeto

Antes de enviar nada, un script puede preguntar qué perfiles pueden actuar sobre una campaña o un anuncio — con todo lo que Qubix sabe sobre su estado:

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

Devuelve un array: una campaña suele ser visible para varios perfiles, sus niveles de acceso en Facebook difieren, y la elección es suya. Cada entrada lleva:

CampoSignificado
ididentificador del perfil — páselo a fb.profile(...)
namenombre del perfil
groupgrupo del perfil
ownerBuyerIdel comprador propietario del perfil
tokenAlivesi el token de acceso está vivo
onCheckpointsi el perfil está atascado en un checkpoint de seguridad de Facebook
hasProxysi el perfil tiene un proxy configurado

Deliberadamente no hay ningún campo «nivel de acceso»: Qubix no lo almacena en ningún sitio. Lo que un perfil puede hacer con un objeto lo responde el propio Facebook — envíe la solicitud y lea la respuesta. tokenAlive, onCheckpoint y hasProxy son estado, no permiso: un perfil con un token muerto permanece en la lista para que usted lo vea y decida por sí mismo. Un objeto fuera de su conjunto de datos devuelve un array vacío, no un error.

La solicitud

request(options) toma un objeto:

OpciónSignificado
urlla dirección absoluta de la solicitud: https://graph.facebook.com/${campaignId}, https://graph.facebook.com/act_123/campaigns, https://graph.facebook.com/me/adaccounts. Funciona cualquier host *.facebook.com — el puente adjunta las cookies y la clave del perfil, y estas nunca van a un host ajeno
methodGET (por defecto), POST o DELETE
paramsparámetros de la solicitud como un objeto; POST los lleva en el cuerpo, los demás métodos — en la dirección
headerssus propias cabeceras; se añaden encima de las nuestras y pueden sobrescribir cualquiera
timeoutMscuánto esperar la respuesta, en milisegundos; vacío — el valor por defecto del operador, y no puede superar el techo

El token de acceso, las cookies, el proxy y la firma del navegador los aporta el propio transporte del perfil.

La respuesta — y los dos tipos de rechazo

La respuesta de Facebook llega tal cual; Qubix no la analiza — usted lo hace:

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
okla respuesta llegó y el código es de éxito
statusel código de respuesta HTTP de Facebook
contentTypeel tipo de contenido de la respuesta
headerslas cabeceras de la respuesta — todas excepto Set-Cookie; aquí es donde vive lo que no está en el cuerpo: los límites de solicitudes restantes, el identificador de la solicitud para un caso de soporte con Facebook
textel cuerpo tal cual, como una cadena — JSON.parse(res.text)
bytesel mismo cuerpo en binario — para imágenes y descargas
profileIdqué perfil de navegador envió la solicitud

Hay dos tipos de rechazo, y llegan de forma diferente:

  • Qubix no pudo entregarla — un proxy caído, una conexión rota, una campaña fuera de su conjunto de datos — se lanza como un error: captúrelo con try/catch si quiere que el bucle continúe. Para lecturas mediante fb.campaign(...), los perfiles alternativos ya se han probado en este punto.
  • Facebook respondió con un rechazo — sin derechos, un campo incorrecto, una sesión caducada — se devuelve como una respuesta normal con res.status >= 400 y el motivo en el cuerpo: JSON.parse(res.text).error lleva message y code, y cuando están presentes error_user_title y error_user_msg, eso es una redacción humana ya lista.

Un ayudante de tres líneas para no repetir el análisis en cada escenario:

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
}
No vuelva a leer justo después de un cambio

Facebook sirve las lecturas con retraso: un valor leído inmediatamente después de un cambio exitoso puede seguir siendo el antiguo. No lo trate como una escritura fallida. Compruebe el nuevo valor en la siguiente ejecución, o recuerde lo que estableció en ctx.state.

Lote: muchas solicitudes en un solo intercambio

La cola por perfil deja pasar una solicitud por intervalo — cien lecturas individuales no caben en el límite de tiempo de la ejecución. Un lote paga la cola una sola 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 }) — envía las subsolicitudes en un solo intercambio; se dividen en bloques de 50 — ese es el límite de Facebook. Un rechazo de todo el lote se lanza como error; compruebe el resultado de cada subsolicitud en su code, y su respuesta analizada en data.
  • fbReads(ids, fields) — construye subsolicitudes de lectura a partir de una lista de identificadores; usted escribe los fields. Una subsolicitud también puede construirse a mano: { method: 'GET', relativeUrl: 'act_123/ads?fields=name' }.

Límites y el registro

  • El tiempo de ejecución del script corta las llamadas a fb igual que al resto del código — el comando es síncrono, como sql y ctx.fetch.
  • La espera por llamada la establece el administrador en SistemaJavaScript, en el bloque Facebook (own request via fb.*): un valor por defecto y un techo. Su timeoutMs se aplica dentro de ese techo.
  • Una cola por perfil. Las solicitudes a un perfil de navegador se espacian en todo el sistema — Britva, la recopilación de estadísticas y los scripts comparten una sola cola, de modo que un script no puede quemar un perfil con llamadas frecuentes. ¿Necesita muchas lecturas? Use un lote.
  • Toda solicitud queda registrada en el registro de solicitudes de su servidor — incluidas las lecturas: hora, script, perfil ejecutor, método, ruta, resultado y la respuesta de Facebook. Una queja como «el script destrozó mis campañas» se aclara en un minuto revisando el registro. Los tokens de acceso nunca llegan al registro — se eliminan de los textos almacenados.

El catálogo de bloques de construcción

Cada una de las solicitudes siguientes ha sido enviada por Qubix en operación real y respondida (verificado el 27 de agosto de 2026) — puede confiar en ellas como conjunto base. Cualquier cosa más allá de esto también funciona: el puente acepta cualquier dirección *.facebook.com — la Graph API y más allá — y los derechos de su perfil deciden el resto. El asistente de IA en el editor de scripts conoce estos bloques y ensamblará un script a partir de una tarea descrita en palabras sencillas.

Los scripts ya hechos construidos a partir de estos bloques están en Ejemplos de scripts.

Quién soy con este perfil

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

Un rechazo aquí significa que la sesión del perfil está muerta. Esta es la forma más barata de comprobar la salud de la sesión según una programación — antes de que el comprador lo note.

Cuentas publicitarias — incluido el dinero

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

El dinero vive aquí — la Graph API no tiene una dirección aparte para el «saldo»:

CampoQué es
amount_spentel gasto total de la cuenta, en unidades menores de la moneda de la cuenta
adtrust_dslel límite de gasto diario de la cuenta
adspaymentcycleel umbral de facturación; el importe está en .data[0].threshold_amount y se divide entre 100
account_statusel estado de la cuenta, como número
disable_reasonel motivo por el que se deshabilitó, como número
currencyla moneda de la cuenta
timezone_namela zona horaria de la cuenta; «hoy» se cuenta según ella

Tarjetas de pago de las cuentas

La misma dirección, un conjunto de campos diferente — display_string lleva la máscara de la tarjeta vinculada:

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

Estado y presupuesto de la campaña

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

daily_budget se devuelve en unidades menores de la moneda de la cuenta, como una cadena. Si el presupuesto está establecido en los conjuntos de anuncios en lugar de en la campaña, el campo simplemente está ausente — eso es un estado normal, no un error.

Cambiar el presupuesto

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

Con éxito, Facebook responde {"success":true}. El escenario completo con umbrales de seguridad es «Aumentar el presupuesto de las campañas rentables» en los ejemplos.

Ajustes del conjunto de anuncios: segmentación, presupuestos, optimización

Un conjunto de anuncios expone lo que nuestros informes no tienen en absoluto:

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 misma manera, una sola solicitud sobre un anuncio extrae sus objetos anidados — la campaña, el conjunto de anuncios y el creativo a la 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}' },
})

Pausar e iniciar

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 misma forma funciona para un anuncio, un conjunto de anuncios y una campaña — Facebook los distingue por el identificador. Pausar una campaña se propaga en cascada: sus anuncios reportan effective_status: 'CAMPAIGN_PAUSED' mientras que su propio status permanece sin cambios. Así que juzgue si algo está realmente en marcha por effective_status, nunca por status.

Prefiera las acciones del SDK para pausar anuncios

Pausar y reactivar anuncios, conjuntos de anuncios y campañas es mejor hacerlo con las funciones integradas .pause() / .activate(): pasan por la misma cola que Britva y mantienen el registro completo de pausas. Use fb para aquello que el SDK no tiene comando.

Por qué un anuncio no entrega

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 lleva el problema en las propias palabras de Facebook; adset.end_time detecta un conjunto de anuncios caducado — la respuesta habitual a «el anuncio está activo pero no gasta nada».

Estadísticas directas desde la cuenta publicitaria

Cuando necesite un corte que los informes de Qubix no tienen, pregúntele al propio 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,
},
})

Dos trampas en las que todos caen:

  • outbound_clicks y unique_outbound_clicks llegan como un array de objetos action_type/value, no como un número — los demás campos de clics llegan como un número dentro de una cadena;
  • no existe en absoluto un campo landing_page_views de nivel superior — solicite actions y busque dentro action_type: 'landing_page_view'.

Imágenes de los creativos — hasta el cuerpo binario

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

Una respuesta de fb también puede ser binaria: res.bytes lleva el cuerpo tal cual — así es como obtiene los propios archivos, no solo enlaces a ellos.

El origen del vídeo y la imagen de la publicación

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

La cadena «anuncio → su imagen» necesita tres solicitudes y no se puede construir de otra manera:

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, Business Manager, publicaciones y comentarios

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 también puede limpiar comentarios por sí solo

La limpieza automática de enlaces plantados, con un registro, ya está integrada — la sección Limpieza de comentarios. Use fb para construir su propia lógica encima: sus propias listas de palabras, sus propias excepciones, sus propias comprobaciones de autor.

Paginación

Facebook sirve las listas largas con cursores: mientras la respuesta lleve paging.next, tome paging.cursors.after y repita la solicitud con after en params. Si no hay paging, es el final.

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 responde «Please reduce the amount of data you're asking for», eso es una petición, no un rechazo: baje el limit y empiece de nuevo.

El poder está en combinarlo con el resto del SDK

El puente se vuelve verdaderamente potente junto con los demás comandos del script: withCondition selecciona objetos según sus estadísticas, sql extrae cualquier corte de la base de datos, fb comprueba y cambia la cuenta publicitaria, ctx.state guarda memoria entre ejecuciones, ctx.fetch envía el resultado a su mensajero.

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

Los objetos también se pueden seleccionar sin SQL — con withCondition: una expresión a nivel de SQL sobre todas las métricas del objeto, con paréntesis, AND/OR/NOT, aritmética y comparación campo a campo — spend_24h > 2 * geo_avg_payout, roas_24h < 0.5 * prev_roas_24h. La lista completa de campos está en la pestaña de macros del editor y en Métricas.

Así es como construye escenarios a los que un proveedor nunca llegaría: su embudo decide, su perfil ejecuta.

Qué sigue