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.
// 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 llevagetProfiles()— 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:
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:
| Campo | Significado |
|---|---|
id | identificador del perfil — páselo a fb.profile(...) |
name | nombre del perfil |
group | grupo del perfil |
ownerBuyerId | el comprador propietario del perfil |
tokenAlive | si el token de acceso está vivo |
onCheckpoint | si el perfil está atascado en un checkpoint de seguridad de Facebook |
hasProxy | si 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ón | Significado |
|---|---|
url | la 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 |
method | GET (por defecto), POST o DELETE |
params | parámetros de la solicitud como un objeto; POST los lleva en el cuerpo, los demás métodos — en la dirección |
headers | sus propias cabeceras; se añaden encima de las nuestras y pueden sobrescribir cualquiera |
timeoutMs | cuá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:
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)
| Campo | Significado |
|---|---|
ok | la respuesta llegó y el código es de éxito |
status | el código de respuesta HTTP de Facebook |
contentType | el tipo de contenido de la respuesta |
headers | las 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 |
text | el cuerpo tal cual, como una cadena — JSON.parse(res.text) |
bytes | el mismo cuerpo en binario — para imágenes y descargas |
profileId | qué 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/catchsi quiere que el bucle continúe. Para lecturas mediantefb.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 >= 400y el motivo en el cuerpo:JSON.parse(res.text).errorllevamessageycode, y cuando están presenteserror_user_titleyerror_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:
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 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:
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 sucode, y su respuesta analizada endata.fbReads(ids, fields)— construye subsolicitudes de lectura a partir de una lista de identificadores; usted escribe losfields. 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
fbigual que al resto del código — el comando es síncrono, comosqlyctx.fetch. - La espera por llamada la establece el administrador en Sistema → JavaScript, en el bloque Facebook (own request via fb.*): un valor por defecto y un techo. Su
timeoutMsse 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
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
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»:
| Campo | Qué es |
|---|---|
amount_spent | el gasto total de la cuenta, en unidades menores de la moneda de la cuenta |
adtrust_dsl | el límite de gasto diario de la cuenta |
adspaymentcycle | el umbral de facturación; el importe está en .data[0].threshold_amount y se divide entre 100 |
account_status | el estado de la cuenta, como número |
disable_reason | el motivo por el que se deshabilitó, como número |
currency | la moneda de la cuenta |
timezone_name | la 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:
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
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
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:
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:
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
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.
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
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:
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_clicksyunique_outbound_clicksllegan como un array de objetosaction_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_viewsde nivel superior — soliciteactionsy busque dentroaction_type: 'landing_page_view'.
Imágenes de los creativos — hasta el cuerpo binario
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
// 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:
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
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}` })
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.
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.
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.