Data Bridge
Lee datos de la tienda directamente desde el frontend de tu plugin usando useWhataloData() — sin necesidad de backend.
useWhataloData() es un hook de React que da a los plugins frontend-only acceso de lectura directo a recursos de la tienda desde el iframe: productos, pedidos, clientes, categorías y metadatos de la tienda. Cada solicitud se envía a través del host administrador de Whatalo (PluginFrame) vía el App Bridge, por lo que no se requiere servidor backend ni API key.
Este es el camino más simple para acceder a datos de la tienda. Si tu plugin solo necesita leer datos — no escribir — puedes prescindir de session tokens, endpoints de backend y gestión de API keys.
Elige la Capa Correcta
Mantén separadas estas tres capas del SDK:
| Necesidad | Herramienta correcta | Por qué |
|---|---|---|
Contexto de sesión y shell (storeId, storeName, locale, theme) | useWhataloContext() | Datos ligeros de arranque del iframe enviados por el host |
Datos de la tienda en solo lectura desde el frontend (orders, products, store.timezone) | useWhataloData() | Bridge de lectura con scopes aplicados por el host y sin API key |
| Lecturas backend, mutaciones, webhooks, exports, lógica confiable del servidor | WhataloClient | Se ejecuta en tu servidor con WHATALO_API_KEY |
Importante: useWhataloContext() no es una API de metadatos de tienda. Si necesitas timezone, currency, domain o ajustes similares en el frontend, léelos mediante useWhataloData().store.get().
Cuándo Usar useWhataloData()
| Escenario | Enfoque Recomendado |
|---|---|
| Dashboard, widget o vista analítica de solo lectura | useWhataloData() — esta página |
| Plugin necesita escribir datos (crear, actualizar, eliminar) | WhataloClient en tu backend — Overview |
| Plugin obtiene datos de su propia base de datos | Session token hacia tu backend — Session Tokens |
Cómo Funciona
El hook envía una acción data del bridge desde el iframe del plugin al host administrador de Whatalo. El host valida que la solicitud proviene de una sesión autenticada, verifica los granted_scopes de la instalación, luego ejecuta la consulta del lado del servidor usando Prisma y retorna el resultado.
Plugin iframe Host admin de Whatalo (PluginFrame) Base de datos
│ │ │
│ whatalo:action { data } │ │
│──────────────────────────► │ │
│ Verificar granted_scopes + sesión auth │
│ │─────────────────────────────►│
│ │ Consulta Prisma (solo lectura)│
│ │◄─────────────────────────────│
│ whatalo:ack { success, data } │ │
│◄────────────────────────── │ │El plugin nunca toca la API de Whatalo ni mantiene ninguna credencial. Todo el control de scopes ocurre dentro del host.
Configuración
1. Declara los scopes requeridos en tu manifiesto
El host rechaza solicitudes de recursos que tu manifiesto no declara. Agrega los scopes para los datos que necesitas:
// whatalo.app.ts
import { defineApp } from "@whatalo/plugin-sdk";
export default defineApp({
id: "my-plugin",
name: "My Plugin",
scopes: [
"read:products", // required for products and categories
"read:orders", // required for orders
"read:customers", // required for customers
"read:store", // required for store metadata
],
adminUI: {
pages: [{ path: "dashboard", title: "Dashboard" }],
},
});2. Importa y llama el hook
import { useWhataloData } from "@whatalo/plugin-sdk/bridge";
export function Dashboard() {
const { products, orders, customers, categories, store } = useWhataloData();
// Los recursos de colección exponen .list(params?) y .get(id)
// Store es un recurso singleton y expone .get()
}3. Reinicia la sesión de desarrollo después de cambiar permisos
Si agregas read:orders, read:customers, read:store o cualquier otro permiso mientras whatalo dev ya está corriendo, detén el proceso y vuelve a iniciarlo. El Data Bridge verifica granted_scopes, no el archivo del manifiesto en disco.
Métodos por Recurso
products
Scope requerido: read:products
// List products — all params are optional
const result = await products.list({
page: 1,
per_page: 20,
status: "active", // "active" | "inactive" — omit for all
search: "camisa", // substring match on product name
});
// result.data — array de objetos Product
// result.pagination.total — total de productos que coinciden
// result.pagination.total_pages — total de páginas
// Obtener un producto por su ID público
const single = await products.get("472819365047");
// single.data — objeto ProductCampos del objeto Product:
| Campo | Tipo | Descripción |
|---|---|---|
id | string | ID público del producto |
name | string | Nombre del producto |
slug | string | Slug amigable para URL |
description | string | null | Descripción del producto |
price | number | Precio de venta |
compare_price | number | null | Precio original (antes del descuento) |
main_image | string | null | URL de la imagen principal |
gallery | string[] | URLs de imágenes adicionales |
is_active | boolean | Si el producto está publicado |
is_featured | boolean | Si el producto está destacado |
sku | string | null | Código de unidad en inventario |
track_inventory | boolean | Si el seguimiento de inventario está habilitado |
stock_quantity | number | Cantidad de stock actual |
condition | string | null | Etiqueta de condición del producto |
is_purchasable | boolean | Derivado: activo y con stock (o sin seguimiento) |
has_discount | boolean | Derivado: compare_price > price |
discount_percentage | number | Derivado: descuento como porcentaje entero |
created_at | string | null | Timestamp ISO 8601 |
updated_at | string | null | Timestamp ISO 8601 |
orders
Scope requerido: read:orders
// List orders — all params are optional
const result = await orders.list({
page: 1,
per_page: 20,
status: "pending", // order status string — omit for all
});
// Obtener un pedido por su ID público
const single = await orders.get("819365047283");Campos del objeto Order (lista):
| Campo | Tipo | Descripción |
|---|---|---|
id | string | ID público del pedido |
order_number | number | Número de pedido legible por humanos |
status | string | Estado del pedido (ej. pending, confirmed, shipped) |
payment_status | string | Estado del pago (ej. pending, paid) |
total | number | Total del pedido |
subtotal | number | Subtotal antes de ajustes |
customer_name | string | Nombre del cliente |
customer_phone | string | Teléfono del cliente |
items_count | number | Número de líneas de producto |
created_at | string | Timestamp ISO 8601 |
updated_at | string | Timestamp ISO 8601 |
Campos adicionales en orders.get() (pedido individual):
| Campo | Tipo | Descripción |
|---|---|---|
payment_method | string | null | Método de pago utilizado |
shipping_method | string | null | Método de envío seleccionado |
discount_amount | number | Descuento aplicado al pedido |
shipping_cost | number | Costo de envío |
tax_amount | number | Impuesto cobrado |
customer | object | Objeto cliente completo: { id, name, phone, email } |
customer_notes | string | null | Notas dejadas por el cliente |
shipping_address | object | null | JSON de la dirección de envío |
items | object[] | Líneas de producto: { product_id, product_name, product_image, variant_name, quantity, unit_price, total_price } |
customers
Scope requerido: read:customers
// List customers — all params are optional
const result = await customers.list({
page: 1,
per_page: 20,
search: "maria", // matches name, phone, or email
});
// Obtener un cliente por su ID público
const single = await customers.get("365047281936");Campos del objeto Customer:
| Campo | Tipo | Descripción |
|---|---|---|
id | string | ID público del cliente |
name | string | Nombre completo |
phone | string | Número de teléfono |
email | string | null | Correo electrónico |
orders_count | number | Total de pedidos realizados |
created_at | string | Timestamp ISO 8601 |
categories
Scope requerido: read:products
Las categorías comparten el scope read:products — declarar read:products en tu manifiesto otorga acceso a ambos recursos.
// List categories — all params are optional
const result = await categories.list({
page: 1,
per_page: 50,
search: "ropa", // substring match on category name
});
// Obtener una categoría por su slug
const single = await categories.get("camisetas");
// Note: categories use slug as their ID, not a public_idCampos del objeto Category:
| Campo | Tipo | Descripción |
|---|---|---|
id | string | Slug de la categoría (usado como ID externo) |
name | string | Nombre de la categoría |
slug | string | Slug amigable para URL |
products_count | number | Número de productos en esta categoría |
is_active | boolean | Si la categoría es visible |
created_at | string | null | Timestamp ISO 8601 |
store
Scope requerido: read:store
Los metadatos de la tienda son un recurso singleton. A diferencia de products.get(id) u orders.get(id), no requiere un ID de recurso.
const result = await store.get();
console.log(result.data.name); // "Benita Store"
console.log(result.data.currency); // "MXN"
console.log(result.data.timezone); // "Europe/Madrid"
console.log(result.data.domain); // "benita.example.com" o nullCampos del objeto Store:
| Campo | Tipo | Descripción |
|---|---|---|
id | string | ID público de la tienda |
name | string | Nombre visible de la tienda |
slug | string | Slug de la tienda |
domain | string | null | Dominio personalizado |
logo | string | null | URL del logo |
description | string | null | Descripción de la tienda |
currency | string | Moneda principal de la tienda |
country | string | Código de país si está configurado |
timezone | string | Zona horaria IANA |
is_active | boolean | Si la tienda está activa |
created_at | string | Timestamp ISO 8601 |
Ejemplo Completo
Un componente dashboard que muestra pedidos recientes y productos con bajo stock:
import { useEffect, useState } from "react";
import { useWhataloData } from "@whatalo/plugin-sdk/bridge";
import type { DataListResponse } from "@whatalo/plugin-sdk/bridge";
interface Order {
id: string;
order_number: number;
status: string;
total: number;
customer_name: string;
}
interface Product {
id: string;
name: string;
stock_quantity: number;
is_purchasable: boolean;
}
export function StoreDashboard() {
const { orders, products, store } = useWhataloData();
const [recentOrders, setRecentOrders] = useState<Order[]>([]);
const [lowStock, setLowStock] = useState<Product[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function loadDashboardData() {
try {
// Both requests run in parallel — no sequential waterfall
const [ordersResult, productsResult, storeResult] = await Promise.all([
orders.list({ page: 1, per_page: 10 }),
products.list({ page: 1, per_page: 100, status: "active" }),
store.get(),
]);
setRecentOrders(ordersResult.data as Order[]);
console.log(`Zona horaria de la tienda: ${storeResult.data.timezone}`);
// Filter client-side for low-stock items
const stockWarning = (productsResult.data as Product[]).filter(
(p) => p.stock_quantity > 0 && p.stock_quantity <= 5
);
setLowStock(stockWarning);
} catch (err) {
setError(err instanceof Error ? err.message : "Error al cargar datos");
} finally {
setLoading(false);
}
}
loadDashboardData();
}, [orders, products, store]);
if (loading) return <p>Cargando dashboard...</p>;
if (error) return <p>Error: {error}</p>;
return (
<div>
<section>
<h2>Pedidos Recientes ({recentOrders.length})</h2>
<ul>
{recentOrders.map((order) => (
<li key={order.id}>
#{order.order_number} — {order.customer_name} — {order.status} —{" "}
${order.total.toFixed(2)}
</li>
))}
</ul>
</section>
<section>
<h2>Alerta de Bajo Stock ({lowStock.length})</h2>
<ul>
{lowStock.map((product) => (
<li key={product.id}>
{product.name} — {product.stock_quantity} restantes
</li>
))}
</ul>
</section>
</div>
);
}Paginación
Todos los métodos .list() retornan un objeto pagination:
const result = await products.list({ page: 1, per_page: 25 });
const { data, pagination } = result;
// pagination.page — número de página actual
// pagination.per_page — ítems retornados por página
// pagination.total — total de ítems coincidentes en todas las páginas
// pagination.total_pages — número total de páginas
// Verificar si hay más páginas
const hasNextPage = pagination.page < pagination.total_pages;Límites de paginación:
- Tamaño de página por defecto:
20ítems - Tamaño máximo de página:
100ítems por solicitud - Solicitar
per_page > 100se recorta silenciosamente a100 - Números de página menores a
1se tratan como1
Límites de Frecuencia
El data bridge aplica un límite de frecuencia de ventana deslizante por instancia de plugin:
- 20 solicitudes por ventana de 10 segundos
- Aplica sobre todos los tipos de recursos combinados —
products.list()yorders.list()cuentan hacia el mismo límite - Cuando se supera el límite, la Promise falla con un mensaje de error
try {
const result = await products.list();
} catch (err) {
// err.message === "Rate limit exceeded"
// Espera antes de reintentar — la ventana se reinicia cada 10 segundos
}El límite de frecuencia se aplica del lado del cliente en el PluginFrame antes de que la solicitud llegue al servidor. Esto es independiente del límite de la API REST de Whatalo que aplica a WhataloClient.
Solución de Problemas del Bridge
Bridge unavailable (sesiones de desarrollo)
En desarrollo local, el bridge puede quedar temporalmente no disponible mientras whatalo dev reinicia o la instalación/sesión queda desactualizada.
Verificaciones rápidas:
- Reinicia
whatalo dev - Vuelve a abrir el plugin desde el admin
- Verifica que el plugin conserva los scopes esperados en
whatalo.app.ts
Comportamiento recomendado del plugin:
- Mostrar un estado de advertencia claro
- Mantener la UI estable
- Reintentar tras recuperar la sesión en lugar de fallar de forma permanente
Manejo de Errores
Cada método retorna una Promise que falla cuando la solicitud falla. Envuelve siempre las llamadas en try/catch:
try {
const result = await orders.list({ page: 1 });
// Usa result.data
} catch (err) {
const message = err instanceof Error ? err.message : "Error desconocido";
// Mensajes de error comunes:
// "Missing scope: read:orders" — scope no declarado o no otorgado
// "Plugin not installed" — registro de instalación no encontrado
// "Rate limit exceeded" — ventana de 20 req/10s agotada
// "Resource ID required for get operation" — .get() llamado sin ID en un recurso no singleton
// "Order not found" — ID no existe en esta tienda
// "Unauthorized" — sesión del usuario inválida
}Referencia de errores:
| Mensaje de error | Causa | Resolución |
|---|---|---|
Missing scope: read:* | Scope no está en el manifiesto o no fue otorgado por el comerciante | Agrega el scope a whatalo.app.ts y pide al comerciante que reinstale |
Plugin not installed | No se encontró instalación activa o en desarrollo | Verifica que el plugin esté instalado y no suspendido |
Rate limit exceeded | Más de 20 solicitudes en 10 segundos | Agrega debouncing o reduce la frecuencia de solicitudes |
* not found | El ID del recurso no pertenece a esta tienda | Verifica que el ID provenga de una llamada list/get anterior en la misma tienda |
Unauthorized | Sesión del admin expirada | El comerciante necesita refrescar la página |
Patrones de Diagnóstico para Plugins de Ejemplo
Si mantienes un plugin de ejemplo para validar el SDK, mantén los diagnósticos enfocados y seguros:
- Incluye pruebas para
Missing scope,* not foundyUnauthorized - Incluye una sonda de disponibilidad del bridge y una sonda de endpoint con sesión
- Maneja
Rate limit exceededcorrectamente, pero evita exponer controles de stress test en la UI pública - Mantén los diagnósticos como opcionales para que el flujo principal del producto siga siendo la prioridad
Resumen de Scopes
| Recurso | Scope requerido |
|---|---|
products | read:products |
orders | read:orders |
customers | read:customers |
categories | read:products |
store | read:store |
Los scopes se declaran una vez en whatalo.app.ts y el comerciante los revisa al momento de instalar. Si un comerciante instala tu plugin sin otorgar un scope, las solicitudes a ese recurso fallarán con "Missing scope: read:*".
useWhataloData() vs. WhataloClient
useWhataloData() | WhataloClient | |
|---|---|---|
| Dónde se ejecuta | Frontend del plugin (componente React) | Backend del plugin (servidor) |
| Credenciales necesarias | Ninguna | WHATALO_API_KEY |
| Operaciones | Solo lectura | Lectura + escritura |
| Recursos | Productos, pedidos, clientes, categorías, store | Los 8 namespaces |
| Límite de frecuencia | 20 req / 10 s (bridge) | 100 req / 10 s (API) |
| Cuándo usarlo | Plugins sin backend | Plugin completo con backend |
Páginas Relacionadas
- Autenticación — API keys, session tokens y cuándo usar cada uno
- Session Tokens — autenticación frontend-to-backend para plugins con servidor
- Scopes y Permisos — cómo declarar y solicitar permisos del comerciante
- Products — referencia completa del recurso products para plugins con backend
- Orders — referencia completa del recurso orders para plugins con backend