API Client

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:

NeedCorrect toolWhy
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 logicWhataloClientRuns 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()

ScenarioRecommended Approach
Read-only dashboard, widget, or analytics viewuseWhataloData() — this page
Plugin needs to write data (create, update, delete)WhataloClient on your backend — Overview
Plugin fetches data from its own databaseSession 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:

  1. Your manifest declares the required permissions
  2. The current installation has those permissions in granted_scopes
  3. 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 object

Product object fields:

FieldTypeDescription
idstringPublic product ID
namestringProduct name
slugstringURL-friendly slug
descriptionstring | nullProduct description
pricenumberSelling price
compare_pricenumber | nullOriginal price (before discount)
main_imagestring | nullPrimary image URL
gallerystring[]Additional image URLs
is_activebooleanWhether the product is published
is_featuredbooleanWhether the product is featured
skustring | nullStock-keeping unit
track_inventorybooleanWhether inventory tracking is enabled
stock_quantitynumberCurrent stock count
conditionstring | nullProduct condition label
is_purchasablebooleanDerived: active and in stock (or no tracking)
has_discountbooleanDerived: compare_price > price
discount_percentagenumberDerived: discount as integer percentage
created_atstring | nullISO 8601 timestamp
updated_atstring | nullISO 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):

FieldTypeDescription
idstringPublic order ID
order_numbernumberHuman-readable order number
statusstringOrder status (e.g., pending, confirmed, in_progress)
payment_statusstringPayment status (e.g., pending, paid)
totalnumberOrder total
subtotalnumberOrder subtotal before adjustments
currencystringStore currency code for the order total (for example USD, DOP, GTQ)
customer_namestringCustomer display name
customer_phonestringCustomer phone number
items_countnumberNumber of line items
created_atstringISO 8601 timestamp
updated_atstringISO 8601 timestamp

Additional fields on orders.get() (single order):

FieldTypeDescription
payment_methodstring | nullPayment method used
shipping_methodstring | nullShipping method selected
discount_amountnumberDiscount applied to the order
shipping_costnumberShipping cost
tax_amountnumberTax charged
currencystringStore currency code for the order
customerobjectFull customer object: { id, name, phone, email } — individual fields may be null for guest or incomplete orders
customer_notesstring | nullNotes left by the customer
shipping_addressobject | nullShipping address JSON
itemsobject[]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:

FieldTypeDescription
idstringPublic customer ID
namestringFull name
phonestringPhone number
emailstring | nullEmail address
orders_countnumberTotal number of orders placed
created_atstringISO 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_id

Category object fields:

FieldTypeDescription
idstringCategory slug (used as external ID)
namestringCategory display name
slugstringURL-friendly slug
products_countnumberNumber of products in this category
is_activebooleanWhether the category is visible
created_atstring | nullISO 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 null

Store object fields:

FieldTypeDescription
idstringPublic store ID
namestringStore display name
slugstringStore slug
domainstring | nullCustom domain
logostring | nullLogo URL
descriptionstring | nullStore description
currencystringPrimary store currency
countrystringStore country code if configured
timezonestringIANA timezone
is_activebooleanWhether the store is active
created_atstringISO 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: 20 items
  • Maximum page size: 100 items per request
  • Requesting per_page > 100 is silently clamped to 100
  • Page numbers below 1 are treated as 1

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() and orders.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 dev after 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 messageCauseResolution
Missing scope: read:*Scope not in manifest or not granted by merchantAdd scope to whatalo.app.ts and ask merchant to re-install
Plugin not installedNo active or development installation foundEnsure the plugin is installed and not suspended
Rate limit exceededMore than 20 requests in 10 secondsAdd debouncing or reduce request frequency
* not foundResource ID does not belong to this storeVerify the ID comes from a previous list/get call to the same store
UnauthorizedAdmin session expiredThe 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, and Unauthorized
  • Include a bridge availability probe and a session-backed endpoint probe
  • Handle Rate limit exceeded gracefully, but avoid exposing stress-test controls in public UI
  • Keep diagnostics optional so normal product flows remain the primary experience

Scopes Summary

ResourceRequired scope
productsread:products
ordersread:orders
customersread:customers
categoriesread:products
storeread: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 runsPlugin frontend (React component)Plugin backend (server)
Credentials neededNoneWHATALO_API_KEY
OperationsRead onlyRead + write
ResourcesProducts, orders, customers, categories, storeAll 8 namespaces
Rate limit20 req / 10 s (bridge)100 req / 10 s (API)
When to useNo-backend pluginsFull plugin with backend
  • 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

On this page