Your First App
This tutorial walks you through building a minimal SeloraX app from scratch. By the end, you will have a working Express.js server that handles the OAuth install flow, makes API calls, and renders inside the merchant dashboard as an iframe.
Prerequisites
- Node.js 18+ installed
- Your app credentials (
client_id,client_secret,session_signing_key) from Creating an App - A SeloraX platform instance running (locally on port 5001 or production)
Project Setup
Create a new directory and initialize your project:
mkdir my-selorax-app && cd my-selorax-app
npm init -y
npm install express axios dotenvCreate a .env file with your credentials:
PORT=5002
CLIENT_ID=sx_app_your_client_id_here
CLIENT_SECRET=sx_secret_your_client_secret_here
SESSION_SIGNING_KEY=your_session_signing_key_here
SELORAX_API_URL=https://api-dev.selorax.io/api
REDIRECT_URI=https://my-app.example.com/oauth/callbackStep 1: Set Up the Express Server
Create index.js with the basic server structure:
require("dotenv").config();
const express = require("express");
const axios = require("axios");
const crypto = require("crypto");
const app = express();
app.use(express.json());
const {
PORT,
CLIENT_ID,
CLIENT_SECRET,
SESSION_SIGNING_KEY,
SELORAX_API_URL,
REDIRECT_URI,
} = process.env;
// In-memory store for tokens (use a database in production)
const tokenStore = {};
app.listen(PORT, () => {
console.log(`App server running on port ${PORT}`);
});Step 2: Handle the OAuth Callback
When a merchant installs your app, the SeloraX platform redirects their browser to your redirect_uri with an authorization code. Add a route to handle this:
app.get("/oauth/callback", async (req, res) => {
const { code, state } = req.query;
if (!code) {
return res.status(400).send("Missing authorization code");
}
console.log(`Authorization code: ${code}`);
try {
// Exchange the authorization code for an access token
const tokenResponse = await axios.post(
`${SELORAX_API_URL}/apps/oauth/token`,
{
grant_type: "authorization_code",
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
code: code,
redirect_uri: REDIRECT_URI,
},
);
const tokenData = tokenResponse.data;
console.log("Token exchange successful:", {
store_id: tokenData.store_id,
installation_id: tokenData.installation_id,
scope: tokenData.scope,
expires_in: tokenData.expires_in,
});
// Store the tokens (use a database in production)
tokenStore[tokenData.store_id] = {
access_token: tokenData.access_token,
refresh_token: tokenData.refresh_token,
installation_id: tokenData.installation_id,
scope: tokenData.scope,
store_id: tokenData.store_id,
};
res.send(`
<html>
<body>
<h1>Installation Successful</h1>
<p>Your app has been installed for store ${tokenData.store_id}.</p>
<p>You can close this window and return to the dashboard.</p>
</body>
</html>
`);
} catch (error) {
console.error(
"Token exchange failed:",
error.response?.data || error.message,
);
res.status(500).send("Installation failed. Please try again.");
}
});Token Exchange Request
The authorization code is short-lived. Your server must exchange it for tokens immediately:
POST /api/apps/oauth/token
{
"grant_type": "authorization_code",
"client_id": "sx_app_1b16e193a28d2640d2d9734dbf4907e8",
"client_secret": "sx_secret_dd0f155b6e333f59acbd3cd5905f2de2fce237fa8f499ecec2605b83eb1b974b",
"code": "sx_ac_a1b2c3d4e5f6...",
"redirect_uri": "https://my-app.example.com/oauth/callback"
}Token Exchange Response
{
"access_token": "sx_at_eyJhbGciOiJIUzI1NiIs...",
"refresh_token": "sx_rt_7f8a9b0c1d2e3f4a5b6c...",
"token_type": "bearer",
"expires_in": 86400,
"scope": "read:orders write:orders read:customers",
"store_id": 22,
"installation_id": 2
}| Field | Description |
|---|---|
access_token | Bearer token for API calls. Expires in expires_in seconds (24 hours). |
refresh_token | Long-lived token used to obtain a new access token when the current one expires. |
token_type | Always "bearer". Include in the Authorization header as Bearer <token>. |
expires_in | Token lifetime in seconds (86400 = 24 hours). |
scope | Space-separated list of granted scopes. |
store_id | The merchant's store ID. Use this to associate the token with the correct store. |
installation_id | Unique identifier for this app installation. |
Step 3: Make Your First API Call
Now that you have an access token, you can call the SeloraX API. Add a route that fetches the store's recent orders:
app.get("/dashboard/orders", async (req, res) => {
const storeId = req.query.store_id;
const tokens = tokenStore[storeId];
if (!tokens) {
return res.status(401).send("App not installed for this store");
}
try {
const response = await axios.get(`${SELORAX_API_URL}/orders`, {
headers: {
Authorization: `Bearer ${tokens.access_token}`,
},
params: {
page: 1,
limit: 10,
},
});
res.json(response.data);
} catch (error) {
console.error("API call failed:", error.response?.data || error.message);
res.status(error.response?.status || 500).json({
error: "Failed to fetch orders",
});
}
});Using Client Credentials (Alternative)
For server-to-server calls where you do not need a user session, you can authenticate with your client credentials directly instead of a Bearer token:
const response = await axios.get(`${SELORAX_API_URL}/orders`, {
headers: {
"X-Client-Id": CLIENT_ID,
"X-Client-Secret": CLIENT_SECRET,
"X-Store-Id": "22",
},
params: {
page: 1,
limit: 10,
},
});Client credentials never expire and work the same as OAuth tokens for data access. Use them for background jobs, cron tasks, and server-side integrations.
Step 4: Serve the Iframe Page
When a merchant opens your app in the dashboard, the platform loads your app_url in an iframe and sends a session token via postMessage. Your frontend needs to:
- Signal that it is ready by posting an
app-bridge:readymessage. - Listen for the session token from the parent frame.
- Use the session token for authenticated frontend requests.
Add a route that serves the iframe HTML:
app.get("/", (req, res) => {
res.send(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My SeloraX App</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
margin: 0;
padding: 24px;
background: #f8f9fa;
color: #1a1a2e;
}
.card {
background: white;
border-radius: 8px;
padding: 24px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
max-width: 600px;
}
.status {
padding: 8px 12px;
border-radius: 4px;
font-size: 14px;
margin-top: 16px;
}
.status.connecting { background: #fff3cd; color: #856404; }
.status.connected { background: #d4edda; color: #155724; }
.status.error { background: #f8d7da; color: #721c24; }
</style>
</head>
<body>
<div class="card">
<h1>My SeloraX App</h1>
<p>This app is running inside the merchant dashboard.</p>
<div id="status" class="status connecting">Connecting to dashboard...</div>
<div id="store-info" style="margin-top: 16px;"></div>
</div>
<script>
let sessionToken = null;
// Step 1: Tell the parent frame we are ready
window.parent.postMessage({ type: "app-bridge:ready" }, "*");
// Step 2: Listen for the session token from the dashboard
window.addEventListener("message", function(event) {
// In production, validate event.origin against your known dashboard URL
var data = event.data;
if (data.type === "selorax:session-token") {
sessionToken = data.token;
var statusEl = document.getElementById("status");
statusEl.className = "status connected";
statusEl.textContent = "Connected to store " + (data.store_id || "");
// Step 3: Use the session token to fetch data
fetchStoreInfo(data.store_id);
}
});
function fetchStoreInfo(storeId) {
fetch("/dashboard/orders?store_id=" + storeId, {
headers: {
"X-Session-Token": sessionToken,
},
})
.then(function(response) { return response.json(); })
.then(function(data) {
var count = (data.data && data.data.length) || 0;
var infoEl = document.getElementById("store-info");
infoEl.textContent = "Recent orders loaded: " + count;
})
.catch(function() {
document.getElementById("store-info").textContent =
"Failed to load data.";
});
}
// Handle timeout
setTimeout(function() {
if (!sessionToken) {
var statusEl = document.getElementById("status");
statusEl.className = "status error";
statusEl.textContent =
"Could not connect to dashboard. Make sure the app is loaded inside the SeloraX admin.";
}
}, 5000);
</script>
</body>
</html>
`);
});Session Token Verification
To verify that a session token is authentic, validate its HMAC signature on your backend using your session_signing_key:
function verifySessionToken(token) {
try {
const [headerB64, payloadB64, signatureB64] = token.split(".");
// Recreate the signature
const message = `${headerB64}.${payloadB64}`;
const expectedSignature = crypto
.createHmac("sha256", SESSION_SIGNING_KEY)
.update(message)
.digest("base64url");
if (signatureB64 !== expectedSignature) {
return null; // Invalid signature
}
// Decode the payload
const payload = JSON.parse(Buffer.from(payloadB64, "base64url").toString());
// Check expiration (tokens are valid for 10 minutes)
if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) {
return null; // Expired
}
return payload;
} catch (err) {
return null;
}
}Complete Code
Here is the full index.js file with all pieces assembled:
require("dotenv").config();
const express = require("express");
const axios = require("axios");
const crypto = require("crypto");
const app = express();
app.use(express.json());
const {
PORT = 5002,
CLIENT_ID,
CLIENT_SECRET,
SESSION_SIGNING_KEY,
SELORAX_API_URL = "https://api-dev.selorax.io/api",
REDIRECT_URI = "https://my-app.example.com/oauth/callback",
} = process.env;
// In-memory store for tokens (use a database in production)
const tokenStore = {};
// ---- Session Token Verification ----
function verifySessionToken(token) {
try {
const [headerB64, payloadB64, signatureB64] = token.split(".");
const message = `${headerB64}.${payloadB64}`;
const expectedSignature = crypto
.createHmac("sha256", SESSION_SIGNING_KEY)
.update(message)
.digest("base64url");
if (signatureB64 !== expectedSignature) return null;
const payload = JSON.parse(Buffer.from(payloadB64, "base64url").toString());
if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) return null;
return payload;
} catch {
return null;
}
}
// ---- OAuth Callback ----
app.get("/oauth/callback", async (req, res) => {
const { code, state } = req.query;
if (!code) {
return res.status(400).send("Missing authorization code");
}
try {
const tokenResponse = await axios.post(
`${SELORAX_API_URL}/apps/oauth/token`,
{
grant_type: "authorization_code",
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
code,
redirect_uri: REDIRECT_URI,
},
);
const tokenData = tokenResponse.data;
tokenStore[tokenData.store_id] = {
access_token: tokenData.access_token,
refresh_token: tokenData.refresh_token,
installation_id: tokenData.installation_id,
scope: tokenData.scope,
store_id: tokenData.store_id,
};
res.send(`
<html><body>
<h1>Installation Successful</h1>
<p>App installed for store ${tokenData.store_id}. You can close this window.</p>
</body></html>
`);
} catch (error) {
console.error(
"Token exchange failed:",
error.response?.data || error.message,
);
res.status(500).send("Installation failed. Please try again.");
}
});
// ---- API Proxy ----
app.get("/dashboard/orders", async (req, res) => {
const storeId = req.query.store_id;
const tokens = tokenStore[storeId];
if (!tokens) {
return res.status(401).json({ error: "App not installed for this store" });
}
try {
const response = await axios.get(`${SELORAX_API_URL}/orders`, {
headers: { Authorization: `Bearer ${tokens.access_token}` },
params: { page: 1, limit: 10 },
});
res.json(response.data);
} catch (error) {
res.status(error.response?.status || 500).json({
error: "Failed to fetch orders",
});
}
});
// ---- Iframe Page ----
app.get("/", (req, res) => {
res.send(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>My SeloraX App</title>
<style>
body { font-family: sans-serif; margin: 0; padding: 24px; background: #f8f9fa; }
.card { background: white; border-radius: 8px; padding: 24px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1); max-width: 600px; }
.status { padding: 8px 12px; border-radius: 4px; font-size: 14px; margin-top: 16px; }
.connecting { background: #fff3cd; color: #856404; }
.connected { background: #d4edda; color: #155724; }
.error { background: #f8d7da; color: #721c24; }
</style>
</head>
<body>
<div class="card">
<h1>My SeloraX App</h1>
<p>Running inside the merchant dashboard.</p>
<div id="status" class="status connecting">Connecting...</div>
<div id="info" style="margin-top:16px"></div>
</div>
<script>
var token = null;
window.parent.postMessage({ type: "app-bridge:ready" }, "*");
window.addEventListener("message", function(e) {
if (e.data.type === "selorax:session-token") {
token = e.data.token;
var s = document.getElementById("status");
s.className = "status connected";
s.textContent = "Connected to store " + (e.data.store_id || "");
fetch("/dashboard/orders?store_id=" + e.data.store_id, {
headers: { "X-Session-Token": token }
}).then(function(r) { return r.json(); }).then(function(d) {
document.getElementById("info").textContent =
"Orders loaded: " + ((d.data && d.data.length) || 0);
});
}
});
setTimeout(function() {
if (!token) {
var s = document.getElementById("status");
s.className = "status error";
s.textContent = "Could not connect. Load this page inside the SeloraX dashboard.";
}
}, 5000);
</script>
</body>
</html>
`);
});
// ---- Start Server ----
app.listen(PORT, () => {
console.log(`App running at port ${PORT}`);
});Run the App
node index.jsYour app is now running at https://my-app.example.com. When a merchant installs it:
- The platform redirects to
https://my-app.example.com/oauth/callback?code=sx_ac_...&state=... - Your server exchanges the code for tokens and stores them
- When the merchant opens the app in their dashboard, the iframe loads
https://my-app.example.com/ - The iframe sends
app-bridge:ready, receives a session token, and fetches order data
Next Steps
- Authentication -- Learn about token refresh, client credentials, and session tokens in depth.
- Webhooks -- Subscribe to events and handle webhook deliveries.
- Billing -- Charge merchants for your app.
- Messaging App Tutorial -- Build a full-featured app with SMS, templates, and order event automation.