Session Tokens
How to use short-lived session tokens to authenticate plugin frontend requests to your backend server.
Session tokens are short-lived JWTs (5-minute expiry) that prove a request originates from an authenticated user within the Whatalo admin. They are the bridge between your plugin's frontend (iframe) and your backend server.
Session tokens do not grant access to the Whatalo API directly. Their purpose is to let your backend know that the incoming request is legitimate before it uses your API key to fetch data on behalf of that user.
Getting a Session Token
In your plugin frontend, request a token from the App Bridge:
import { sessionToken } from "@whatalo/plugin-sdk/bridge";
// Returns a token and its expiration time
const { token, expiresAt } = await sessionToken();
// Send it as a Bearer token to your backend
const response = await fetch("/api/orders", {
headers: {
Authorization: `Bearer ${token}`,
},
});
const data = await response.json();The App Bridge caches the token and returns the cached copy on subsequent calls. When the token is within 60 seconds of expiry, the next call to sessionToken() fetches a new one transparently — you do not need to manage this yourself.
Verifying on Your Backend
Use verifyWhataloSessionToken from @whatalo/plugin-sdk/server to validate the token before trusting the request:
import { verifyWhataloSessionToken } from "@whatalo/plugin-sdk/server";
import { WhataloClient } from "@whatalo/plugin-sdk";
export async function GET(request: Request) {
const authHeader = request.headers.get("Authorization") ?? "";
const token = authHeader.replace("Bearer ", "");
if (!token) {
return new Response("Missing authorization header", { status: 401 });
}
// Verify the token — throws if invalid or expired
let claims;
try {
claims = verifyWhataloSessionToken(token, process.env.WHATALO_CLIENT_SECRET!);
} catch (error) {
return new Response("Invalid or expired session token", { status: 401 });
}
// Token is valid — use your API key to call the Whatalo API
const client = new WhataloClient({ apiKey: process.env.WHATALO_API_KEY! });
const orders = await client.orders.list({ page: 1, per_page: 25 });
return Response.json(orders);
}verifyWhataloSessionToken throws an Error if the token is expired, the signature does not match, or the token structure is invalid. Catch it with a standard try/catch block.
Token Claims
The decoded token contains the following claims:
| Claim | Type | Description |
|---|---|---|
iss | "whatalo" | Token issuer — always "whatalo" |
aud | string | Your plugin's client ID |
sub | string | Store ID (same as storeId) |
exp | number | Expiration timestamp (Unix, UTC) |
iat | number | Issued-at timestamp (Unix, UTC) |
jti | string | Unique token ID — prevents replay attacks |
storeId | string | Public store identifier |
appId | string | Your plugin's public ID |
scopes | string[] | Scopes granted by the merchant at install time |
installationId | string | Installation record ID |
Checking Scopes
If your endpoint requires a specific permission scope, check it after verifying the token:
const claims = verifyWhataloSessionToken(token, process.env.WHATALO_CLIENT_SECRET!);
if (!claims.scopes.includes("read:orders")) {
return new Response("Insufficient permissions", { status: 403 });
}
// Scope confirmed — proceed
const orders = await client.orders.list();Auto-Refresh
The App Bridge handles token lifecycle automatically:
- Cache hit: If the cached token has more than 60 seconds remaining,
sessionToken()returns it immediately without a network request. - Proactive refresh: If the token has 60 seconds or less remaining, a new token is fetched before returning.
- Concurrent calls: Multiple simultaneous calls to
sessionToken()share a single in-flight refresh — only one network request is made.
You do not need to store tokens, track expiry times, or schedule refresh logic. Call sessionToken() before every request to your backend.
During development, whatalo dev tunnels a single app origin. Relative browser calls such as /api/orders stay on that tunneled origin when your frontend and backend share the same local app server.
Full Request Example
A complete frontend-to-backend flow fetching store orders:
import { useEffect, useState } from "react";
import { sessionToken } from "@whatalo/plugin-sdk/bridge";
interface Order {
id: string;
status: string;
total: number;
}
function OrderList() {
const [orders, setOrders] = useState<Order[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function fetchOrders() {
try {
// Always get a fresh (or cached) session token
const { token } = await sessionToken();
const res = await fetch("/api/orders", {
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) {
throw new Error(`Request failed: ${res.status}`);
}
const data = await res.json();
setOrders(data);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to load orders");
} finally {
setLoading(false);
}
}
fetchOrders();
}, []);
if (loading) return <p>Loading orders...</p>;
if (error) return <p>Error: {error}</p>;
return (
<ul>
{orders.map((order) => (
<li key={order.id}>
{order.id} — {order.status} — ${order.total}
</li>
))}
</ul>
);
}Security Notes
- Always verify on your backend — never trust the token without calling
verifyWhataloSessionToken. The token is signed; an unverified token provides no security guarantee. - Never forward the session token to the Whatalo API — it is not accepted as a Whatalo API credential. Use your
WHATALO_API_KEYfor Whatalo API calls. - Use HTTPS in production — session tokens must only be transmitted over encrypted connections.
- Do not store session tokens server-side — they are short-lived by design. Fetch a new one for each user action.
- Check the
jticlaim for sensitive operations — for actions that must not be replayed (e.g., creating an order), record usedjtivalues and reject duplicates.
Related Pages
- Authentication — the complete two-method auth model with wrong vs. correct patterns
- Context & Session — how the App Bridge provides store and user context
- Scopes and Permissions — declaring what your plugin can access