SeloraXDEVELOPERS

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 dotenv

Create 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/callback

Step 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
}
FieldDescription
access_tokenBearer token for API calls. Expires in expires_in seconds (24 hours).
refresh_tokenLong-lived token used to obtain a new access token when the current one expires.
token_typeAlways "bearer". Include in the Authorization header as Bearer <token>.
expires_inToken lifetime in seconds (86400 = 24 hours).
scopeSpace-separated list of granted scopes.
store_idThe merchant's store ID. Use this to associate the token with the correct store.
installation_idUnique 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:

  1. Signal that it is ready by posting an app-bridge:ready message.
  2. Listen for the session token from the parent frame.
  3. 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.js

Your app is now running at https://my-app.example.com. When a merchant installs it:

  1. The platform redirects to https://my-app.example.com/oauth/callback?code=sx_ac_...&state=...
  2. Your server exchanges the code for tokens and stores them
  3. When the merchant opens the app in their dashboard, the iframe loads https://my-app.example.com/
  4. 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.