Data Bridge
Read store data directly from your plugin frontend using useWhataloData() — no backend required.
useWhataloData() is a React hook that gives frontend-only plugins direct read access to store resources from the iframe: products, orders, customers, categories, and store metadata. Every request is proxied through the Whatalo admin host (PluginFrame) via the App Bridge, so no backend server or API key is required.
This is the simplest path to store data. If your plugin only needs to read data — not write it — you can skip session tokens, backend endpoints, and API key management entirely.
Choose the Right Layer
Keep these three SDK layers separate:
| Need | Correct tool | Why |
|---|---|---|
Session and shell context (storeId, storeName, locale, theme) | useWhataloContext() | Lightweight iframe bootstrap data from the host |
Read-only store data in the frontend (orders, products, store.timezone) | useWhataloData() | Read bridge with host-enforced scopes and no API key |
| Backend reads, mutations, webhooks, exports, trusted server logic | WhataloClient | Runs on your server with WHATALO_API_KEY |
Important: useWhataloContext() is not a store metadata API. If you need timezone, currency, domain, or similar store settings in the frontend, read them through useWhataloData().store.get().
When to Use useWhataloData()
| Scenario | Recommended Approach |
|---|---|
| Read-only dashboard, widget, or analytics view | useWhataloData() — this page |
| Plugin needs to write data (create, update, delete) | WhataloClient on your backend — Overview |
| Plugin fetches data from its own database | Session token to your backend — Session Tokens |
How It Works
The hook sends a data bridge action from the plugin iframe to the Whatalo admin host. The host validates that the request originates from an authenticated session, checks the installation's granted_scopes, then executes the query server-side using Prisma and returns the result.
Plugin iframe Whatalo admin host (PluginFrame) Database
│ │ │
│ whatalo:action { data } │ │
│──────────────────────────► │ │
│ Check granted_scopes + auth session │
│ │─────────────────────────────►│
│ │ Prisma query (read-only) │
│ │◄─────────────────────────────│
│ whatalo:ack { success, data } │ │
│◄────────────────────────── │ │The plugin never touches the Whatalo API or holds any credentials. All scope enforcement happens inside the host.
Requirements
The Data Bridge works only when all of the following are true:
- Your manifest declares the required
permissions - The current installation has those permissions in
granted_scopes - The request runs inside an authenticated plugin session
In development, whatalo dev creates a temporary installation with status: "development" and syncs granted_scopes from the manifest you loaded in that session. If you change permissions, restart whatalo dev so the dev installation is recreated or re-synced before testing the bridge.
The bridge identifies the current plugin by its public identifier. In bridge actions this value may be the plugin slug rather than the internal UUID, so host-side resolution must treat it as a public app identifier, not assume a database UUID.
Setup
1. Declare required scopes in your manifest
The host rejects requests for resources your manifest does not declare. Add the scopes for the data you need:
// 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. Import and call the hook
import { useWhataloData } from "@whatalo/plugin-sdk/bridge";
export function Dashboard() {
const { products, orders, customers, categories, store } = useWhataloData();
// Collection resources expose .list(params?) and .get(id)
// Store is a singleton resource and exposes .get()
}3. Restart the dev session after permission changes
If you add read:orders, read:customers, read:store, or any other new permission while whatalo dev is already running, stop it and start it again. The Data Bridge checks granted_scopes, not the raw manifest file on disk.
Resource Methods
products
Required scope: 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: "shirt", // substring match on product name
});
// result.data — array of Product objects
// result.pagination.total — total matching products
// result.pagination.total_pages — total pages
// Fetch a single product by its public ID
const single = await products.get("472819365047");
// single.data — Product objectProduct object fields:
| Field | Type | Description |
|---|---|---|
id | string | Public product ID |
name | string | Product name |
slug | string | URL-friendly slug |
description | string | null | Product description |
price | number | Selling price |
compare_price | number | null | Original price (before discount) |
main_image | string | null | Primary image URL |
gallery | string[] | Additional image URLs |
is_active | boolean | Whether the product is published |
is_featured | boolean | Whether the product is featured |
sku | string | null | Stock-keeping unit |
track_inventory | boolean | Whether inventory tracking is enabled |
stock_quantity | number | Current stock count |
condition | string | null | Product condition label |
is_purchasable | boolean | Derived: active and in stock (or no tracking) |
has_discount | boolean | Derived: compare_price > price |
discount_percentage | number | Derived: discount as integer percentage |
created_at | string | null | ISO 8601 timestamp |
updated_at | string | null | ISO 8601 timestamp |
orders
Required scope: 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
});
// Fetch a single order by its public ID
const single = await orders.get("819365047283");Order object fields (list):
| Field | Type | Description |
|---|---|---|
id | string | Public order ID |
order_number | number | Human-readable order number |
status | string | Order status (e.g., pending, confirmed, in_progress) |
payment_status | string | Payment status (e.g., pending, paid) |
total | number | Order total |
subtotal | number | Order subtotal before adjustments |
currency | string | Store currency code for the order total (for example USD, DOP, GTQ) |
customer_name | string | Customer display name |
customer_phone | string | Customer phone number |
items_count | number | Number of line items |
created_at | string | ISO 8601 timestamp |
updated_at | string | ISO 8601 timestamp |
Additional fields on orders.get() (single order):
| Field | Type | Description |
|---|---|---|
payment_method | string | null | Payment method used |
shipping_method | string | null | Shipping method selected |
discount_amount | number | Discount applied to the order |
shipping_cost | number | Shipping cost |
tax_amount | number | Tax charged |
currency | string | Store currency code for the order |
customer | object | Full customer object: { id, name, phone, email } — individual fields may be null for guest or incomplete orders |
customer_notes | string | null | Notes left by the customer |
shipping_address | object | null | Shipping address JSON |
items | object[] | Line items: { product_id, product_name, product_image, variant_name, quantity, unit_price, total_price } |
Currency values returned by the bridge are real store/order currency codes from Whatalo data. Do not hardcode a display currency in your plugin UI; always format totals with the currency field returned by the order payload.
customers
Required scope: 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
});
// Fetch a single customer by their public ID
const single = await customers.get("365047281936");Customer object fields:
| Field | Type | Description |
|---|---|---|
id | string | Public customer ID |
name | string | Full name |
phone | string | Phone number |
email | string | null | Email address |
orders_count | number | Total number of orders placed |
created_at | string | ISO 8601 timestamp |
categories
Required scope: read:products
Categories share the read:products scope — declaring read:products in your manifest grants access to both resources.
// List categories — all params are optional
const result = await categories.list({
page: 1,
per_page: 50,
search: "ropa", // substring match on category name
});
// Fetch a single category by its slug
const single = await categories.get("camisetas");
// Note: categories use slug as their ID, not a public_idCategory object fields:
| Field | Type | Description |
|---|---|---|
id | string | Category slug (used as external ID) |
name | string | Category display name |
slug | string | URL-friendly slug |
products_count | number | Number of products in this category |
is_active | boolean | Whether the category is visible |
created_at | string | null | ISO 8601 timestamp |
store
Required scope: read:store
Store metadata is a singleton resource. Unlike products.get(id) or orders.get(id), it does not require a resource ID.
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" or nullStore object fields:
| Field | Type | Description |
|---|---|---|
id | string | Public store ID |
name | string | Store display name |
slug | string | Store slug |
domain | string | null | Custom domain |
logo | string | null | Logo URL |
description | string | null | Store description |
currency | string | Primary store currency |
country | string | Store country code if configured |
timezone | string | IANA timezone |
is_active | boolean | Whether the store is active |
created_at | string | ISO 8601 timestamp |
Complete Example
A dashboard component that displays recent orders and low-stock products side by side:
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;
currency: string;
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(`Store timezone: ${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 : "Failed to load data");
} finally {
setLoading(false);
}
}
loadDashboardData();
}, [orders, products, store]);
if (loading) return <p>Loading dashboard...</p>;
if (error) return <p>Error: {error}</p>;
return (
<div>
<section>
<h2>Recent Orders ({recentOrders.length})</h2>
<ul>
{recentOrders.map((order) => (
<li key={order.id}>
#{order.order_number} — {order.customer_name} — {order.status} —{" "}
{new Intl.NumberFormat("es-DO", {
style: "currency",
currency: order.currency,
}).format(order.total)}
</li>
))}
</ul>
</section>
<section>
<h2>Low Stock Alert ({lowStock.length})</h2>
<ul>
{lowStock.map((product) => (
<li key={product.id}>
{product.name} — {product.stock_quantity} remaining
</li>
))}
</ul>
</section>
</div>
);
}Pagination
All .list() methods return a pagination object:
const result = await products.list({ page: 1, per_page: 25 });
const { data, pagination } = result;
// pagination.page — current page number
// pagination.per_page — items returned per page
// pagination.total — total matching items across all pages
// pagination.total_pages — total number of pages
// Check if there are more pages
const hasNextPage = pagination.page < pagination.total_pages;Pagination limits:
- Default page size:
20items - Maximum page size:
100items per request - Requesting
per_page > 100is silently clamped to100 - Page numbers below
1are treated as1
Rate Limits
The data bridge enforces a sliding-window rate limit per plugin instance:
- 20 requests per 10-second window
- Applies across all resource types combined —
products.list()andorders.list()count toward the same limit - When the limit is exceeded, the Promise rejects with an error message
try {
const result = await products.list();
} catch (err) {
// err.message === "Rate limit exceeded"
// Wait before retrying — the window resets every 10 seconds
}The rate limit is enforced client-side in the PluginFrame before the request reaches the server. This is separate from the Whatalo REST API rate limit that applies to WhataloClient.
Troubleshooting
Data request failed
The host rejected the bridge action. Check the browser console or host logs for the exact error message returned in the bridge ack.
Bridge unavailable (dev sessions)
In local development, the bridge can be temporarily unavailable while whatalo dev restarts or the installation/session is stale.
Quick checks:
- Restart
whatalo dev - Re-open the plugin from the admin
- Verify the plugin still has the expected scopes in
whatalo.app.ts
Recommended plugin behavior:
- Show a clear warning state
- Keep the UI responsive
- Retry after session recovery instead of failing permanently
Insufficient permissions
Your plugin asked for a resource without the matching scope in granted_scopes.
- Confirm the permission is declared in
whatalo.app.ts - Restart
whatalo devafter changing permissions - Re-open the plugin from the admin after the new dev session is active
Internal error
This means the host accepted the request but the server-side resolver failed while building the response. This is a host-side bug, not a reason to add fallback credentials in the plugin frontend. Check the Whatalo server logs and the bridge action payload before changing plugin code.
Error Handling
Every method returns a Promise that rejects when the request fails. Always wrap calls in try/catch:
try {
const result = await orders.list({ page: 1 });
// Use result.data
} catch (err) {
const message = err instanceof Error ? err.message : "Unknown error";
// Common error messages:
// "Missing scope: read:orders" — scope not declared in manifest or not granted
// "Plugin not installed" — installation record not found
// "Rate limit exceeded" — 20 req/10s window exhausted
// "Resource ID required for get operation" — called .get() without an ID on a non-singleton resource
// "Order not found" — ID does not exist in this store
// "Unauthorized" — user session is invalid
}Error reference:
| Error message | Cause | Resolution |
|---|---|---|
Missing scope: read:* | Scope not in manifest or not granted by merchant | Add scope to whatalo.app.ts and ask merchant to re-install |
Plugin not installed | No active or development installation found | Ensure the plugin is installed and not suspended |
Rate limit exceeded | More than 20 requests in 10 seconds | Add debouncing or reduce request frequency |
* not found | Resource ID does not belong to this store | Verify the ID comes from a previous list/get call to the same store |
Unauthorized | Admin session expired | The merchant needs to refresh the page |
Diagnostics Patterns for Example Plugins
If you maintain a sample plugin to validate SDK behavior, keep diagnostics focused and safe:
- Include checks for
Missing scope,* not found, andUnauthorized - Include a bridge availability probe and a session-backed endpoint probe
- Handle
Rate limit exceededgracefully, but avoid exposing stress-test controls in public UI - Keep diagnostics optional so normal product flows remain the primary experience
Scopes Summary
| Resource | Required scope |
|---|---|
products | read:products |
orders | read:orders |
customers | read:customers |
categories | read:products |
store | read:store |
Scopes are declared once in whatalo.app.ts and reviewed by the merchant at install time. If a merchant installs your plugin without granting a scope, requests to that resource will fail with "Missing scope: read:*".
useWhataloData() vs. WhataloClient
useWhataloData() | WhataloClient | |
|---|---|---|
| Where it runs | Plugin frontend (React component) | Plugin backend (server) |
| Credentials needed | None | WHATALO_API_KEY |
| Operations | Read only | Read + write |
| Resources | Products, orders, customers, categories, store | All 8 namespaces |
| Rate limit | 20 req / 10 s (bridge) | 100 req / 10 s (API) |
| When to use | No-backend plugins | Full plugin with backend |
Related Pages
- Authentication — API keys, session tokens, and when to use each
- Session Tokens — frontend-to-backend auth for plugins with a server
- Scopes and Permissions — how to declare and request merchant permissions
- Products — full product resource reference for backend plugins
- Orders — full order resource reference for backend plugins