Modo Desarrollador
Cómo conectar P4D con tu propio backend para alimentar el catálogo de un agente desde una API externa y recibir las órdenes que el agente genera en tu sistema.
¿Para quién es esto?
El modo Desarrollador está pensado para integradores que ya tienen un sistema propio (e-commerce, ERP, base de productos) y quieren que el agente de P4D lea desde ahí en lugar de cargar productos a mano.
Hoy hay dos features:
- Catálogo custom por API — el agente ejecuta código JS que vos escribís para traer productos desde tu API cada vez que un usuario pregunta.
-
Webhook de órdenes — cuando el agente genera una orden,
P4D hace
POSTa la URL que vos configures con todo el detalle.
Ventas → Catálogo → tab Desarrollador.
Catálogo custom por API
En lugar de cargar productos manualmente en la tabla del agente, escribís un snippet de JavaScript que se ejecuta cada vez que un cliente pregunta por productos. Tu código consulta la API que vos quieras y devuelve la lista normalizada al shape que el agente espera.
Activar el modo
Arriba de la pantalla de catálogo hay un switch Fuente activa con dos opciones: Manual y Desarrollador. Solo una está activa a la vez; los productos manuales no se borran al cambiar.
Editor de código
El editor es Monaco (mismo que VS Code), con highlighting, autocomplete
sobre las variables disponibles y validación TypeScript en vivo. El código
corre dentro de un sandbox aislado del lado de P4D — no podés acceder a
process, require, fs ni nada del
sistema operativo.
Snippets predefinidos
En el botón Insertar snippet arriba del editor podés copiar uno de estos templates para arrancar:
- Bearer token simple — API REST con token fijo en header.
- OAuth2 client_credentials — pide access_token y consulta.
- HMAC firmado — para APIs que requieren firma (Shopify-like).
- Búsqueda multi-keyword — itera
query + keywordsy deduplica.
Variables disponibles dentro del sandbox
| Variable | Tipo | Descripción |
|---|---|---|
query | string | El texto del usuario tal como llega del agente (ej. "manzana"). |
keywords | string[] | Sinónimos / términos relacionados generados por el agente. Puede venir vacío. |
phone | string | Teléfono del usuario (canal WhatsApp). Vacío en otros canales. |
botId | string | UUID del agente que ejecuta la consulta. Útil si tu API requiere multi-tenant. |
creds | object | Objeto con tus credenciales guardadas en P4D (ver Credenciales). |
fetch | function | El fetch estándar (proxy al host para HTTPS). |
crypto | object | Subset del módulo crypto: createHmac, createHash, randomBytes, randomUUID. |
log | function | Tipo console.log pero enmascara automáticamente cualquier valor de creds antes de escribir a los logs internos. |
process, require, fs,
child_process, eval con strings dinámicos del
host, ni globalThis del host. Cada invocación es un
Isolate nuevo de 128 MB que se descarta al terminar.
Shape de salida esperado
Tu código tiene que devolver un array con objetos de la forma:
[
{
id: string | number, // requerido — identificador único
nombre: string, // requerido
precio: number | string, // requerido
imagen?: string | null, // opcional — URL pública de la imagen
metadata?: Record<string, unknown> | null // opcional — lo que quieras
},
...
]
Devolver array vacío (return []) es válido — significa "no
encontré productos para esta consulta". El validador de shape usa
Zod y rechaza
cualquier item que no matchee.
Ejemplo mínimo funcional (OpenFoodFacts)
// Búsqueda contra OpenFoodFacts, sin auth, en español.
const url = "https://world.openfoodfacts.org/api/v2/search?"
+ "search_terms=" + encodeURIComponent(query)
+ "&lang=es&page_size=10"
+ "&fields=code,product_name,product_name_es,brands,image_url";
const res = await fetch(url);
if (!res.ok) throw new Error("HTTP " + res.status);
const data = await res.json();
return data.products.map(p => ({
id: p.code,
nombre: p.product_name_es || p.product_name || p.brands || "Sin nombre",
precio: 1000, // OFF no trae precios — poné tu lógica
imagen: p.image_url,
metadata: { marca: p.brands, codigo_barras: p.code }
}));
Aprovechar keywords para búsqueda enriquecida
// El agente puede generar sinónimos del input ("manzana" → ["fruta", "manzana fuji"])
const terms = [query, ...keywords].filter(Boolean);
const seen = new Map();
for (const term of terms) {
const res = await fetch("https://api.tu-tienda.com/products?q=" + encodeURIComponent(term), {
headers: { Authorization: "Bearer " + creds.apiKey }
});
if (!res.ok) continue;
const data = await res.json();
for (const p of data.items) {
if (!seen.has(p.id)) {
seen.set(p.id, {
id: p.id, nombre: p.title, precio: p.price, imagen: p.image_url,
metadata: { matched: term }
});
}
}
}
return Array.from(seen.values()).slice(0, 20);
Credenciales
En la sección Credenciales del editor podés guardar secrets que necesite tu API (API keys, tokens, client_secret, etc).
- Se guardan por nombre, ej:
apiKey,clientSecret. - Dentro del código se acceden como
creds.apiKey. - Son write-only: una vez guardadas no se pueden leer desde la UI — solo el servicio de P4D las desencripta para inyectarlas al sandbox.
- Para cambiar el valor, lo sobreescribís; para borrarla, eliminás la fila.
log(...) reemplaza
automáticamente cualquier substring que coincida con un valor de
creds antes de imprimir en los logs internos. Usalo en lugar de
console.log para no filtrar secrets accidentalmente.
Validar el código
El botón Validar manda tu código a un LLM (Claude Haiku) que chequea estáticamente:
- Que devuelva un array con el shape correcto.
- Que no haya intentos de acceder a
process,require, etc. - Que no haya patrones de exfiltración (mandar todo
credsa un endpoint). - Que no haya loops infinitos obvios.
Es una primera barrera, no la única — el sandbox bloquea físicamente esos accesos aunque la validación los deje pasar.
Probar ejecución
El panel Probar ejecución dispara tu código contra
valores de prueba que vos definís: query, phone,
keywords (separadas por coma).
El resultado te muestra:
- Status OK o Error con el mensaje.
- Tiempo de ejecución en ms.
- Tabla de "Productos mapeados" si el shape es válido.
- "Valor devuelto" (raw) cuando el array está vacío o el shape falló.
Casos en los que Test run devuelve error
| Caso | Cómo se ve |
|---|---|
| Shape inválido | INVALID_OUTPUT + detalle de qué campo falta. |
| Excepción en tu código | RUNTIME_ERROR + mensaje del throw. |
| Timeout (>10s) | TIMEOUT — el Isolate corta la ejecución. |
| Memory exceeded (>128MB) | Falla silenciosa del Isolate. |
Fallo de red en fetch | RUNTIME_ERROR con mensaje de red. |
Guardar y publicar
- Editás el código en Monaco.
- Click Validar — si pasa, se habilita Guardar.
- Click Guardar.
- Movés el switch Fuente activa a Desarrollador.
Desde ese momento, todas las consultas de catálogo del agente pasan por tu código. Si querés rollback, movés el switch de vuelta a Manual.
Límites del sandbox
| Límite | Valor |
|---|---|
| Timeout por invocación | 10 segundos |
| Memoria por Isolate | 128 MB |
| Tamaño de respuesta del array | ~6 MB |
| Lenguaje | JavaScript (ES2022, async/await) |
| Módulos importables | ninguno — solo las globals expuestas |
Webhook de órdenes
Cuando el agente completa una orden, P4D hace POST al endpoint
que vos configures con el detalle completo. Sirve para inyectar la orden
en tu sistema (e-commerce, ERP, base interna).
La card Webhook de órdenes aparece abajo del catálogo dentro del tab Desarrollador.
Configurar
- Pegá la URL pública de tu endpoint (HTTPS) en el campo URL del webhook.
- Click Guardar. P4D genera automáticamente un secret aleatorio (244 bits de entropía).
- Se abre un modal con el secret en plano — copialo ahora, no se vuelve a mostrar.
- Pegalo en una variable de entorno de tu backend (ej.
P4D_WEBHOOK_SECRET). - Activá el toggle Inactivo → Activo para que P4D empiece a disparar el webhook.
Payload
P4D manda POST con Content-Type: application/json y este body:
{
"id": "uuid de la orden",
"bot_id": "uuid del agente",
"customer": {
"name": "Nombre del cliente",
"phone": "+5491100000000",
"email": "cliente@ejemplo.com"
},
"items": [
{
"product_id": "id que devolviste en el catálogo",
"product_name": "Nombre del producto",
"quantity": 1,
"price": 1000
}
],
"total": 1000,
"status": "pending",
"created_at": "2026-06-02T18:00:00.000Z"
}
El payload del botón Probar es idéntico pero con un campo
extra "test": true y todos los IDs en cero, para que puedas
diferenciar mocks de órdenes reales en tu backend.
Verificación del secret
En cada POST P4D manda el header x-nodo-webhook-secret con
el valor que copiaste al crear el webhook. Tu backend solo tiene que
comparar ese header con la variable de entorno donde lo guardaste.
Ejemplo en Node.js / Express
app.post("/webhooks/p4d-orders", express.json(), (req, res) => {
if (req.headers["x-nodo-webhook-secret"] !== process.env.P4D_WEBHOOK_SECRET) {
return res.status(401).send("Unauthorized");
}
const order = req.body;
// ... procesar la orden ...
res.status(200).send("ok");
});
Ejemplo en Python / FastAPI
from fastapi import FastAPI, Header, HTTPException, Request
import os
app = FastAPI()
@app.post("/webhooks/p4d-orders")
async def handle(req: Request, x_nodo_webhook_secret: str = Header(None)):
if x_nodo_webhook_secret != os.environ["P4D_WEBHOOK_SECRET"]:
raise HTTPException(status_code=401)
order = await req.json()
# ... procesar ...
return {"ok": True}
Probar
Click Probar dispara un POST a tu URL con un payload mock (cliente "Cliente de prueba", item demo, total $1000). Te muestra:
- Status HTTP que devolvió tu endpoint.
- Tiempo de respuesta en ms.
- Body de respuesta (los primeros 2KB) si lo querés inspeccionar.
Historial de intentos
Al final de la card hay un desplegable Últimos intentos que muestra los últimos 10 POSTs hechos a tu URL, separando los de prueba (test) de los reales (live):
- Fecha y hora.
- Estado (OK / Error).
- Código HTTP recibido.
- Tiempo de respuesta.
Rotar el secret
Si perdés el secret o sospechás que se filtró, click Rotar en la sección Secret. Se genera uno nuevo, se muestra una vez, y el anterior queda inválido inmediatamente. Acordate de actualizar la env var de tu backend.
Errores comunes
"Servicio no configurado" o errores 401/500 desde Validar / Test
Problema del lado de P4D, no del código tuyo. Contactá a soporte si persiste.
"INVALID_OUTPUT" en Test run
Tu código no devolvió un array, o algún item no tiene
id / nombre / precio. Mirá la
descripción del error que apunta al campo problemático y al "Valor devuelto"
para ver el raw.
Webhook devuelve 401/403 desde tu backend
Tu endpoint está rechazando el secret. Verificá:
- Que la env var del backend tenga el valor exacto que copiaste del modal (sin espacios).
- Que estés leyendo el header case-insensitive. Algunos frameworks normalizan a
x-nodo-webhook-secret(todo minúsculas). - Si rotaste el secret, que la env var nueva esté deployada.
El agente dice que no hay productos pero la API tiene
- El switch Fuente activa está en Manual, no en Desarrollador.
- Tu código devuelve
[]para esaquery. Probalo en Test run con ese mismo texto. - El timeout de 10s te está cortando — agregá
log("paso 1")para ver cuándo se queda.
FAQ
¿Puedo usar npm install <algo> dentro del sandbox?
No. El sandbox no tiene require ni import — solo
las globals expuestas. Si necesitás una librería específica, encapsulala
del lado de tu API y consumila vía fetch.
¿Puedo cachear el access_token de OAuth entre invocaciones?
No en runtime (cada invocación es un Isolate nuevo). Sí podés cachearlo del lado de tu API y pedirlo con un endpoint propio que el sandbox consulte. Es la forma estándar de manejar OAuth con webhooks/serverless.
¿Cuánto tarda una consulta del agente al catálogo?
Típicamente 50-150ms del lado de P4D + lo que tarde tu API. Si tu API es la mayor parte del tiempo, optimizala vos.
¿Qué pasa si mi API está caída cuando el agente consulta?
Tu código tira excepción, P4D devuelve un error genérico, y el agente le contesta al usuario algo tipo "No pudimos consultar el catálogo en este momento". Los detalles quedan en los logs internos para diagnóstico.
¿Hay reintentos del webhook de órdenes?
Hoy no — un solo intento, sin retry, con 10s de timeout. Si tu endpoint está caído, la orden queda registrada en P4D pero no llega a tu backend. Si necesitás retries con backoff, lo agregamos en una próxima iteración.
¿Puedo recibir webhooks de otros eventos (ej. lead nuevo, mensaje)?
Por ahora solo órdenes. Si te interesa otro evento, escribinos.