Cliente API

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:

NecesidadHerramienta correctaPor 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 servidorWhataloClientSe 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()

EscenarioEnfoque Recomendado
Dashboard, widget o vista analítica de solo lecturauseWhataloData() — esta página
Plugin necesita escribir datos (crear, actualizar, eliminar)WhataloClient en tu backend — Overview
Plugin obtiene datos de su propia base de datosSession 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 Product

Campos del objeto Product:

CampoTipoDescripción
idstringID público del producto
namestringNombre del producto
slugstringSlug amigable para URL
descriptionstring | nullDescripción del producto
pricenumberPrecio de venta
compare_pricenumber | nullPrecio original (antes del descuento)
main_imagestring | nullURL de la imagen principal
gallerystring[]URLs de imágenes adicionales
is_activebooleanSi el producto está publicado
is_featuredbooleanSi el producto está destacado
skustring | nullCódigo de unidad en inventario
track_inventorybooleanSi el seguimiento de inventario está habilitado
stock_quantitynumberCantidad de stock actual
conditionstring | nullEtiqueta de condición del producto
is_purchasablebooleanDerivado: activo y con stock (o sin seguimiento)
has_discountbooleanDerivado: compare_price > price
discount_percentagenumberDerivado: descuento como porcentaje entero
created_atstring | nullTimestamp ISO 8601
updated_atstring | nullTimestamp 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):

CampoTipoDescripción
idstringID público del pedido
order_numbernumberNúmero de pedido legible por humanos
statusstringEstado del pedido (ej. pending, confirmed, shipped)
payment_statusstringEstado del pago (ej. pending, paid)
totalnumberTotal del pedido
subtotalnumberSubtotal antes de ajustes
customer_namestringNombre del cliente
customer_phonestringTeléfono del cliente
items_countnumberNúmero de líneas de producto
created_atstringTimestamp ISO 8601
updated_atstringTimestamp ISO 8601

Campos adicionales en orders.get() (pedido individual):

CampoTipoDescripción
payment_methodstring | nullMétodo de pago utilizado
shipping_methodstring | nullMétodo de envío seleccionado
discount_amountnumberDescuento aplicado al pedido
shipping_costnumberCosto de envío
tax_amountnumberImpuesto cobrado
customerobjectObjeto cliente completo: { id, name, phone, email }
customer_notesstring | nullNotas dejadas por el cliente
shipping_addressobject | nullJSON de la dirección de envío
itemsobject[]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:

CampoTipoDescripción
idstringID público del cliente
namestringNombre completo
phonestringNúmero de teléfono
emailstring | nullCorreo electrónico
orders_countnumberTotal de pedidos realizados
created_atstringTimestamp 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_id

Campos del objeto Category:

CampoTipoDescripción
idstringSlug de la categoría (usado como ID externo)
namestringNombre de la categoría
slugstringSlug amigable para URL
products_countnumberNúmero de productos en esta categoría
is_activebooleanSi la categoría es visible
created_atstring | nullTimestamp 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 null

Campos del objeto Store:

CampoTipoDescripción
idstringID público de la tienda
namestringNombre visible de la tienda
slugstringSlug de la tienda
domainstring | nullDominio personalizado
logostring | nullURL del logo
descriptionstring | nullDescripción de la tienda
currencystringMoneda principal de la tienda
countrystringCódigo de país si está configurado
timezonestringZona horaria IANA
is_activebooleanSi la tienda está activa
created_atstringTimestamp 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 > 100 se recorta silenciosamente a 100
  • Números de página menores a 1 se tratan como 1

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() y orders.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 errorCausaResolución
Missing scope: read:*Scope no está en el manifiesto o no fue otorgado por el comercianteAgrega el scope a whatalo.app.ts y pide al comerciante que reinstale
Plugin not installedNo se encontró instalación activa o en desarrolloVerifica que el plugin esté instalado y no suspendido
Rate limit exceededMás de 20 solicitudes en 10 segundosAgrega debouncing o reduce la frecuencia de solicitudes
* not foundEl ID del recurso no pertenece a esta tiendaVerifica que el ID provenga de una llamada list/get anterior en la misma tienda
UnauthorizedSesión del admin expiradaEl 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 found y Unauthorized
  • Incluye una sonda de disponibilidad del bridge y una sonda de endpoint con sesión
  • Maneja Rate limit exceeded correctamente, 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

RecursoScope requerido
productsread:products
ordersread:orders
customersread:customers
categoriesread:products
storeread: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 ejecutaFrontend del plugin (componente React)Backend del plugin (servidor)
Credenciales necesariasNingunaWHATALO_API_KEY
OperacionesSolo lecturaLectura + escritura
RecursosProductos, pedidos, clientes, categorías, storeLos 8 namespaces
Límite de frecuencia20 req / 10 s (bridge)100 req / 10 s (API)
Cuándo usarloPlugins sin backendPlugin 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

On this page