@selorax/app-sdk
v2.1.0
Published
SDK for building apps on the SeloraX platform — session auth, platform API client, webhook verification
Downloads
215
Readme
selorax-app-sdk
Build apps for the SeloraX e-commerce platform
Getting Started • Guides • Extensions • API Reference • Examples • Troubleshooting
Installation
npm install selorax-app-sdk[!NOTE] Peer dependency: Your project must have
expressv4 or v5 installed.
| Requirement | Version | Notes |
|:------------|:--------|:------|
| Node.js | ≥ 18 | Uses native fetch |
| Express | v4 or v5 | Peer dependency |
| SeloraX Developer Account | — | Register here to get credentials. See docs for guides. |
Getting Started
1. Get your credentials
Register your app on the SeloraX Developer Portal. You'll receive:
| Credential | Prefix | Example |
|:-----------|:------:|:--------|
| Client ID | sx_app_ | sx_app_1b16e193a28d2640... |
| Client Secret | sx_secret_ | sx_secret_dd0f155b6e333f59... |
| Session Signing Key | — | ac4d5804b66820c347f626... |
| Webhook Secret | whsec_ | whsec_8a3f1d2e5b7c9a0f... |
2. Configure environment
Create a .env file:
# ━━━ Required ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
SELORAX_API_URL=https://api.selorax.io/api
SELORAX_CLIENT_ID=sx_app_your_client_id
SELORAX_CLIENT_SECRET=sx_secret_your_client_secret
# ━━━ Optional ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
SESSION_SIGNING_KEY=your_64_char_hex_signing_key
WEBHOOK_SIGNING_SECRET=whsec_your_webhook_secret3. Write your first app
require('dotenv').config();
const express = require('express');
const { seloraxAuth, seloraxApi, createWebhookRouter } = require('selorax-app-sdk');
const app = express();
app.use(express.json({
verify: (req, _res, buf) => { req.rawBody = buf.toString(); },
}));
// Webhook events from the platform
app.use('/webhooks', createWebhookRouter({
'order.status_changed': (event, req, res) => {
console.log(`Order #${event.data.order_id} → ${event.data.status}`);
res.json({ message: 'OK', status: 200 });
},
}));
// Your app's dashboard API (protected)
app.get('/api/dashboard', seloraxAuth, async (req, res) => {
const store = await seloraxApi.store.get(req.session.store_id);
res.json({ data: store.data });
});
app.listen(5010);Guides
Authentication
The problem: When a merchant opens your app in the SeloraX dashboard, the app loads inside an iframe. You need to securely identify which merchant is using your app — without asking them to log in again.
The solution: The dashboard sends a short-lived session token to your iframe via postMessage. Your frontend passes it to your backend in every request. The SDK verifies it automatically.
How the token flows
┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
│ SeloraX Dashboard │ │ Your Frontend │ │ Your Backend │
│ │ │ (iframe) │ │ (Express) │
└─────────┬───────────┘ └─────────┬───────────┘ └─────────┬───────────┘
│ │ │
│ 1. Load iframe │ │
│ ?store_id=22&host=... │ │
│──────────────────────────────>│ │
│ │ │
│ 2. Session token │ │
│ via postMessage │ │
│──────────────────────────────>│ │
│ │ │
│ │ 3. GET /api/dashboard │
│ │ X-Session-Token: eyJ... │
│ │──────────────────────────────>│
│ │ │
│ │ 4. seloraxAuth
│ │ verifies │
│ │ ↓ │
│ │ req.session = │
│ │ { store_id, │
│ │ install_id,│
│ │ app_id } │
│ │ │
│ │ 5. JSON response │
│ │<──────────────────────────────│Usage
const { seloraxAuth } = require('selorax-app-sdk');
app.get('/api/my-feature', seloraxAuth, (req, res) => {
// req.session is populated after verification:
const { store_id, installation_id, app_id } = req.session;
res.json({ store_id });
});Verification modes
The middleware picks the best available method automatically:
| Mode | Condition | Speed | How it works |
|:-----|:----------|:-----:|:-------------|
| Local | SESSION_SIGNING_KEY is set | < 1ms | Verifies JWT signature locally with HMAC-SHA256. No network call. |
| Platform | Signing key not set | ~50ms | Calls /apps/session/verify with your client credentials. Results cached for 5 minutes. |
[!TIP] Set
SESSION_SIGNING_KEYin production for the fastest possible auth. Without it, the SDK still works — it just makes an HTTP call to the platform (cached).
Platform API
The problem: Your app needs to read and write merchant data — orders, products, customers, billing. You'd have to handle authentication headers, URL building, error parsing, and query parameters for every request.
The solution: seloraxApi provides a namespaced client that handles all of this. Just call the method with a storeId and get back parsed JSON.
const { seloraxApi } = require('selorax-app-sdk');[!IMPORTANT] Every API call authenticates using your
SELORAX_CLIENT_IDandSELORAX_CLIENT_SECRET. These are sent as headers on every request and never expire — similar to Shopify's offline access tokens.
// List with pagination and filters
const orders = await seloraxApi.orders.list(storeId, {
page: 1,
limit: 20,
});
// Get a single order by ID
const order = await seloraxApi.orders.get(storeId, orderId);Response structure:
{
"message": "Orders fetched.",
"data": [
{
"id": 1234,
"order_number": "SX-1234",
"status": "processing",
"total": 1500,
"customer_name": "John Doe",
"customer_phone": "01700000000"
}
],
"status": 200
}const products = await seloraxApi.products.list(storeId, { page: 1 });
const product = await seloraxApi.products.get(storeId, productId);const customers = await seloraxApi.customers.list(storeId, { page: 1 });
const customer = await seloraxApi.customers.get(storeId, userId);// Returns name, currency, domain, settings, etc.
const store = await seloraxApi.store.get(storeId);const inventory = await seloraxApi.inventory.list(storeId, { page: 1 });All merchant payments flow through the SeloraX platform:
// ── One-time or recurring charges ──────────────────
// Create a charge (merchant sees an approval page)
const charge = await seloraxApi.billing.createCharge(storeId, {
name: 'Pro Plan',
amount: 500,
type: 'one_time', // or 'recurring'
});
// → charge.data.confirmation_url (redirect merchant here)
// Check if merchant approved
const status = await seloraxApi.billing.getCharge(storeId, chargeId);
// ── Wallet (for usage-based billing) ───────────────
// Check balance
const wallet = await seloraxApi.billing.getWallet(storeId);
// Debit per-usage (e.g., SMS sent, API call made)
await seloraxApi.billing.debitWallet(storeId, 10, 'SMS delivery', {
sms_id: 456,
phone: '01700000000',
});
// Request a top-up (merchant pays via payment gateway)
await seloraxApi.billing.createTopup(storeId, 500);// List current subscriptions
const subs = await seloraxApi.webhooks.list(storeId);
// Subscribe to an event
await seloraxApi.webhooks.create(storeId, {
topic: 'order.status_changed',
url: 'https://myapp.com/webhooks/receive',
});// GET
const data = await seloraxApi.apiCall(storeId, 'GET', '/apps/v1/custom-endpoint');
// POST with body
const result = await seloraxApi.apiCall(storeId, 'POST', '/apps/v1/custom-endpoint', {
key: 'value',
});Error handling
All methods throw on non-2xx responses with a structured error:
try {
await seloraxApi.orders.get(storeId, 'bad-id');
} catch (err) {
err.message // → "Order not found"
err.status // → 404
err.data // → { message: "Order not found", status: 404 }
}Webhooks
The problem: When events happen on the platform — an order ships, your app gets installed or uninstalled — you need to react in real time. The platform sends signed HTTP requests to your app, but you need to verify they're genuine.
The solution: The SDK provides two approaches: a router factory that handles everything, or a low-level verify function for custom setups.
[!WARNING] Your Express app must capture the raw request body for HMAC verification. Add this before all routes:
app.use(express.json({ verify: (req, _res, buf) => { req.rawBody = buf.toString(); }, }));
Option A: Router Factory (recommended)
One function — creates a complete webhook endpoint with signature verification:
const { createWebhookRouter } = require('selorax-app-sdk');
const webhooks = createWebhookRouter({
'order.status_changed': (event, req, res) => {
const { store_id, data } = event;
console.log(`[Store ${store_id}] Order #${data.order_id} → "${data.status}"`);
// Your logic here:
// - Send push notification
// - Update your database
// - Trigger an automation
res.json({ message: 'Processed', status: 200 });
},
'app.installed': (event, req, res) => {
console.log(`Installed on store ${event.store_id}`);
// Initialize default settings for this store...
res.json({ message: 'OK', status: 200 });
},
'app.uninstalled': (event, req, res) => {
console.log(`Uninstalled from store ${event.store_id}`);
// Clean up data for this store...
res.json({ message: 'OK', status: 200 });
},
});
// Mounts at: POST /webhooks/receive
app.use('/webhooks', webhooks);The event object:
{
topic: 'order.status_changed', // event name
store_id: 22, // originating store
data: { // event-specific payload
order_id: 1234,
order_number: 'SX-1234',
status: 'shipped',
customer_name: 'John Doe',
customer_phone: '01700000000',
total: 1500,
store_name: 'My Store',
}
}Option B: Manual Verification
Full control for custom setups:
const { verifyWebhook } = require('selorax-app-sdk');
app.post('/my-endpoint', (req, res) => {
const isValid = verifyWebhook(
req.rawBody, // raw body string
req.header('X-SeloraX-Signature'), // "sha256=<hex>"
req.header('X-SeloraX-Timestamp'), // Unix seconds
process.env.WEBHOOK_SIGNING_SECRET // "whsec_..."
);
if (!isValid) return res.status(401).json({ message: 'Bad signature' });
// Process the verified event...
res.json({ message: 'OK' });
});Security guarantees
| Protection | How it works |
|:-----------|:-------------|
| Authenticity | HMAC-SHA256 signature over timestamp.body proves the request came from SeloraX |
| Timing attack prevention | Signatures compared with crypto.timingSafeEqual |
| Replay attack prevention | Requests older than 5 minutes are automatically rejected |
Token Store
The problem: After a merchant installs your app via OAuth, you receive access and refresh tokens. You need to store them somewhere and retrieve them later when making API calls on behalf of that store.
The solution: A simple key-value store keyed by store_id. Ships with an in-memory implementation for development. Swap the backend for production.
const { tokenStore } = require('selorax-app-sdk');| Method | Returns | Description |
|:-------|:--------|:------------|
| get(storeId) | object \| null | Retrieve stored tokens |
| set(storeId, data) | void | Store tokens |
| remove(storeId) | void | Delete tokens |
| getAll() | array | List all stored tokens |
// After OAuth code exchange — store the tokens
tokenStore.set(storeId, {
access_token: 'sx_at_...',
refresh_token: 'sx_rt_...',
installation_id: 5,
scopes: ['read_orders', 'read_products'],
expires_at: '2025-06-01T00:00:00Z',
});
// Later — retrieve tokens for API calls
const tokens = tokenStore.get(storeId);
// → { store_id, access_token, refresh_token, installation_id, scopes, expires_at, updated_at }
// On uninstall — clean up
tokenStore.remove(storeId);[!CAUTION] The built-in store is in-memory only — all tokens are lost when the server restarts. For production, replace with MySQL, Redis, or any persistent storage.
Extensions
The problem: You want your app's UI to appear natively inside the SeloraX dashboard — on the main dashboard, order detail pages, product pages, etc. — without building an iframe.
The solution: Define your UI as a JSON component tree in extensions.json, register it via the dev portal API, and handle backend calls when the dashboard proxies requests to your server.
Extension JSON UI
Extensions use a declarative JSON format. Each node has a type, props, and optional children:
{
"type": "Card",
"props": { "title": "My Widget" },
"children": [
{ "type": "Text", "props": { "content": "Hello {{state.name}}" } },
{
"type": "Button",
"props": { "label": "Click Me" },
"actions": {
"onClick": {
"type": "call_backend",
"url": "/api/extensions/my-action",
"method": "POST"
}
}
}
]
}Available constants
const { EXTENSION_TARGETS, EXTENSION_COMPONENTS, EXTENSION_ACTIONS } = require('selorax-app-sdk');
// 26 targets — where extensions can appear
EXTENSION_TARGETS.DASHBOARD_WIDGET // 'dashboard.widget'
EXTENSION_TARGETS.ORDER_DETAIL_BLOCK // 'order.detail.block'
EXTENSION_TARGETS.NAVIGATION_LINK // 'navigation.link'
// 50+ component types — what you can render
EXTENSION_COMPONENTS.Card // 'Card'
EXTENSION_COMPONENTS.Text // 'Text'
EXTENSION_COMPONENTS.Button // 'Button'
EXTENSION_COMPONENTS.TextArea // 'TextArea'
EXTENSION_COMPONENTS.Badge // 'Badge'
// 8 action types — what can happen on interaction
EXTENSION_ACTIONS.CALL_BACKEND // 'call_backend'
EXTENSION_ACTIONS.SET_STATE // 'set_state'
EXTENSION_ACTIONS.NAVIGATE // 'navigate'Template syntax
String props support {{expression}} templates:
| Expression | Example | Description |
|:-----------|:--------|:------------|
| State access | {{state.count}} | Read from extension's local state |
| Context access | {{context.order.order_id}} | Read from page context (e.g., current order) |
| Negation | {{!state.loaded}} | Boolean negation |
| Input value | {{$value}} | Current form input value (in onChange actions) |
| Interpolation | "Total: {{state.amount}}" | Inline string interpolation |
Conditional rendering
Any component can have a when prop. It renders only when the expression is truthy:
{ "type": "Text", "props": { "when": "{{state.loaded}}", "content": "Ready!" } }
{ "type": "Spinner", "props": { "when": "{{!state.loaded}}" } }Response directives
When the dashboard proxies a call_backend action to your server, your response can include directives:
res.json({
update_state: { count: 42, loaded: true }, // Patch local state
show_toast: { message: 'Saved!', type: 'success' }, // Toast notification
navigate: '/3/orders', // Redirect user
open_modal: { title: 'Details', ui: { ... } }, // Open a modal
close_modal: true, // Close current modal
refetch: ['ORDERS_KEY'], // Invalidate React Query caches
});Registration
Use scripts/register-extensions.js in the boilerplate, or call the dev portal API directly:
# Register
curl -X POST http://localhost:4301/api/v1/apps/{APP_ID}/extensions \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "extension_id": "my-widget", "target": "dashboard.widget", ... }'
# Deploy (makes extensions live)
curl -X POST http://localhost:4301/api/v1/apps/{APP_ID}/extensions/deploy \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "extensions": [...], "message": "v1" }'API Reference
Exports
const {
seloraxAuth, // Express middleware
seloraxApi, // Platform API client
createWebhookRouter, // Webhook router factory
verifyWebhook, // Manual HMAC verification
tokenStore, // Token storage
EXTENSION_TARGETS, // Extension target constants
EXTENSION_COMPONENTS, // Valid component types
EXTENSION_ACTIONS, // Valid action types
} = require('selorax-app-sdk');seloraxAuth
| Aspect | Details |
|:-------|:--------|
| Type | Express middleware (req, res, next) |
| Reads | X-Session-Token header |
| Sets | req.session → { store_id: number, installation_id: number, app_id: number } |
| Errors | 401 — missing or invalid token · 500 — platform verification failed |
seloraxApi
createWebhookRouter(handlers)
| Param | Type | Description |
|:------|:-----|:------------|
| handlers | { [topic]: (event, req, res) => void } | Event topic → handler map |
Returns express.Router with route POST /receive.
verifyWebhook(rawBody, signature, timestamp, secret)
| Param | Type | Description |
|:------|:-----|:------------|
| rawBody | string | Raw HTTP request body |
| signature | string | X-SeloraX-Signature header value |
| timestamp | string | X-SeloraX-Timestamp header value |
| secret | string | Your webhook signing secret |
Returns boolean — true if valid signature and timestamp within 5 minutes.
tokenStore
| Method | Returns | Description |
|:-------|:--------|:------------|
| .get(storeId) | object \| null | Retrieve stored tokens |
| .set(storeId, data) | void | Store token data |
| .remove(storeId) | void | Delete stored tokens |
| .getAll() | array | List all stored token objects |
Full Example
require('dotenv').config();
const express = require('express');
const {
seloraxAuth,
seloraxApi,
createWebhookRouter,
tokenStore,
} = require('selorax-app-sdk');
const app = express();
// ── Raw body capture (required for webhook HMAC) ───────────
app.use(express.json({
verify: (req, _res, buf) => { req.rawBody = buf.toString(); },
}));
// ── OAuth Install Callback ─────────────────────────────────
app.post('/oauth/callback', async (req, res) => {
const { code } = req.body;
const tokenRes = await fetch(`${process.env.SELORAX_API_URL}/apps/oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'authorization_code',
client_id: process.env.SELORAX_CLIENT_ID,
client_secret: process.env.SELORAX_CLIENT_SECRET,
code,
redirect_uri: 'http://localhost:5010/oauth/callback',
}),
});
const data = await tokenRes.json();
tokenStore.set(data.data.store_id, data.data);
console.log(`Installed on store ${data.data.store_id}`);
res.json({ message: 'Installed', status: 200 });
});
// ── Webhooks ───────────────────────────────────────────────
app.use('/webhooks', createWebhookRouter({
'order.status_changed': (event, req, res) => {
console.log(`[Store ${event.store_id}] Order #${event.data.order_id} → ${event.data.status}`);
res.json({ message: 'OK', status: 200 });
},
'app.uninstalled': (event, req, res) => {
tokenStore.remove(event.store_id);
console.log(`Uninstalled from store ${event.store_id}`);
res.json({ message: 'OK', status: 200 });
},
}));
// ── Protected App Routes ───────────────────────────────────
app.get('/api/dashboard', seloraxAuth, async (req, res) => {
const { store_id } = req.session;
const [store, orders, wallet] = await Promise.all([
seloraxApi.store.get(store_id),
seloraxApi.orders.list(store_id, { limit: 5 }),
seloraxApi.billing.getWallet(store_id),
]);
res.json({
store: store.data,
recent_orders: orders.data,
wallet_balance: wallet.data?.balance,
});
});
// ── Start ──────────────────────────────────────────────────
app.listen(5010, () => console.log('App running on http://localhost:5010'));Architecture
┌──────────────────────────────────────────────────────────────────┐
│ SeloraX Merchant Dashboard │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Your App (iframe) │ │
│ │ │ │
│ │ Frontend (Next.js) Backend (Express) │ │
│ │ ┌──────────────┐ ┌──────────────────┐ │ │
│ │ │ │ HTTP + │ │ │ │
│ │ │ App Bridge │ X-Session │ seloraxAuth │ │ │
│ │ │ handles │──────────────>│ verifies token │ │ │
│ │ │ postMessage │ Token │ │ │ │ │
│ │ │ tokens │ │ ▼ │ │ │
│ │ │ │ │ seloraxApi │ │ │
│ │ └──────────────┘ │ calls platform ─┼─────┼──┼──> SeloraX
│ │ │ │ │ │ Platform
│ │ │ Webhook Router │<────┼──┼── API
│ │ │ verifies HMAC │ │ │
│ │ └──────────────────┘ │ │
│ └────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘Data flow:
| Step | What happens |
|:----:|:-------------|
| 1 | Merchant opens your app in the dashboard → iframe loads |
| 2 | Dashboard sends a session token to the iframe via postMessage |
| 3 | Frontend makes API calls to your backend with X-Session-Token |
| 4 | seloraxAuth verifies the token → populates req.session |
| 5 | Your backend calls seloraxApi to read/write platform data |
| 6 | Platform sends webhook events (order changes, installs) to your backend |
| 7 | createWebhookRouter verifies HMAC signature → routes to your handler |
Troubleshooting
Your .env file is missing credentials. Get them from the Developer Portal. Make sure you're calling require('dotenv').config() before importing the SDK.
Session tokens expire after 10 minutes. Your frontend should automatically request fresh tokens from the dashboard via postMessage. If testing manually, you'll need to get a new token from the dashboard.
Neither verification method is available. You need at least one of:
SESSION_SIGNING_KEYset in.env(for local verification), orSELORAX_CLIENT_ID+SELORAX_CLIENT_SECRET+SELORAX_API_URLall set (for platform verification)
Three things to check:
WEBHOOK_SIGNING_SECRETmatches the secret shown in the Developer Portal- You have the raw body capture middleware before your routes:
app.use(express.json({ verify: (req, _res, buf) => { req.rawBody = buf.toString(); } })); - The request body hasn't been modified between capture and verification
The SDK rejects webhooks older than 5 minutes. This usually means your server's clock is off. Fix with:
# Linux
sudo ntpdate pool.ntp.org
# macOS (usually auto-synced)
sudo sntp -sS pool.ntp.orgYour app doesn't have the required permission scope. Check the scopes you requested during app registration on the Developer Portal. Common scopes: read_orders, read_products, read_customers, read_inventory.
Links
| Resource | URL | |:---------|:----| | Documentation | docs.selorax.io | | Developer Portal | portal.selorax.io | | npm SDK | @selorax/cli | | npm UI Kit | @selorax/ui | | App Boilerplate | github.com/SeloraX-io/selorax-app-boilerplate | | SeloraX Platform | selorax.io |
License
MIT
