Skip to content

VIO Mini App Integration Guide

This document provides a comprehensive guide for building third-party mini apps that integrate with the VIO platform.

Table of Contents


1. Overview

What is a VIO Mini App?

A VIO mini app is a third-party web application that gets embedded inside the VIO Member App via an iframe. Mini apps can access VIO platform features like:

  • User information (profile, wallet address)
  • Token balances and transfers
  • Voucher listings and issuance
  • User's claimed vouchers

Architecture

┌─────────────────────────────────────────────────────────────────┐
│                      VIO Member App                              │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │                     MiniAppPage                            │  │
│  │  ┌─────────────────────────────────────────────────────┐  │  │
│  │  │                    <iframe>                          │  │  │
│  │  │                                                      │  │  │
│  │  │              YOUR MINI APP                           │  │  │
│  │  │                                                      │  │  │
│  │  │    ┌──────────────────────────────────────────┐     │  │  │
│  │  │    │  1. Parse URL params (refreshToken)      │     │  │  │
│  │  │    │  2. Exchange for accessToken             │     │  │  │
│  │  │    │  3. Call VIO Mini App API                │     │  │  │
│  │  │    └──────────────────────────────────────────┘     │  │  │
│  │  │                       │                              │  │  │
│  │  └───────────────────────│──────────────────────────────┘  │  │
│  └──────────────────────────│─────────────────────────────────┘  │
└─────────────────────────────│────────────────────────────────────┘


                    ┌─────────────────────┐
                    │    VIO API Server   │
                    │  /api/miniapp/*     │
                    └─────────────────────┘

Key Points

  • No API Key Required: Mini apps use JWT-based authentication (refresh/access tokens), not API keys
  • Iframe Embedding: Your app runs inside an iframe in the VIO Member App
  • Direct API Access: Your mini app communicates directly with the VIO API server
  • User Context: The logged-in user's credentials are passed to your app via URL parameters

2. How Your Mini App Gets Launched

Iframe Loading

When a user taps on your mini app icon in the VIO Member App, the app navigates to:

/{tenantSlug}/mini-app/{itemId}

The VIO Member App then loads your mini app URL in an iframe, appending context parameters as query strings.

URL Parameters Passed to Your App

Your mini app URL will receive these query parameters:

ParameterTypeDescription
tenantstringThe tenant slug (e.g., acme)
companystringSame as tenant slug
userIdstringThe current user's ID (MongoDB ObjectId)
refreshTokenstringJWT refresh token prefixed with Bearer
redirectUrlstringBase URL to redirect users back to VIO (e.g., https://app.vio.com/acme)

Example URL your app receives:

https://your-miniapp.com/?tenant=acme&company=acme&userId=65f1a2b3c4d5e6f7a8b9c0d1&refreshToken=Bearer%20eyJhbGci...&redirectUrl=https%3A%2F%2Fapp.vio.com%2Facme

Iframe Sandbox Attributes

Your mini app runs with these sandbox permissions:

html
<iframe
  sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox"
  allow="geolocation; camera; microphone"
/>

What this means:

PermissionDescription
allow-scriptsJavaScript execution is allowed
allow-same-originCan access same-origin resources and storage
allow-formsForm submission is allowed
allow-popupsCan open new windows/tabs
allow-popups-to-escape-sandboxOpened popups are not sandboxed
geolocationCan request user's location
cameraCan access camera
microphoneCan access microphone

3. Authentication Flow

Overview

┌──────────────┐         ┌──────────────┐         ┌──────────────┐
│  VIO Member  │         │  Your Mini   │         │   VIO API    │
│     App      │         │     App      │         │   Server     │
└──────┬───────┘         └──────┬───────┘         └──────┬───────┘
       │                        │                        │
       │  Load iframe with      │                        │
       │  ?refreshToken=xxx     │                        │
       │───────────────────────>│                        │
       │                        │                        │
       │                        │  GET /api/miniapp/api/auth/token
       │                        │  Authorization: Bearer {refreshToken}
       │                        │───────────────────────>│
       │                        │                        │
       │                        │  { accessToken: "Bearer ..." }
       │                        │<───────────────────────│
       │                        │                        │
       │                        │  GET /api/miniapp/api/user/info
       │                        │  Authorization: Bearer {accessToken}
       │                        │───────────────────────>│
       │                        │                        │
       │                        │  { userId, username, email, ... }
       │                        │<───────────────────────│
       │                        │                        │

Step-by-Step

Step 1: Parse the Refresh Token from URL

When your mini app loads, extract the refreshToken from the URL query parameters:

javascript
const urlParams = new URLSearchParams(window.location.search);
const refreshToken = urlParams.get('refreshToken');
const tenant = urlParams.get('tenant');
const userId = urlParams.get('userId');
const redirectUrl = urlParams.get('redirectUrl');

Step 2: Exchange Refresh Token for Access Token

Call the token exchange endpoint with the refresh token:

bash
curl -X GET "https://api.vio.com/api/miniapp/api/auth/token" \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

Response (Success):

json
{
  "status": "Success",
  "message": {
    "accessToken": "Bearer eyJhbGciOiJIUzI1NiIs..."
  }
}

Step 3: Use Access Token for API Calls

All subsequent API calls must include the access token in the Authorization header:

bash
curl -X GET "https://api.vio.com/api/miniapp/api/user/info" \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Token Lifetimes

Token TypeLifetimeUsage
Refresh Token7 daysExchange for access token
Access Token15 minutesAPI authentication

TIP

If the access token expires, exchange the refresh token again to get a new one.


4. API Reference

Base URL: https://api.vio.com/api/miniapp

All successful responses follow this format:

json
{
  "status": "Success",
  "message": { ... }
}

4.1 Exchange Token

Exchange a refresh token for an access token.

Endpoint: GET /api/miniapp/api/auth/token

Authentication: Refresh token in Authorization header

Request:

http
GET /api/miniapp/api/auth/token HTTP/1.1
Host: api.vio.com
Authorization: Bearer {refreshToken}

Response:

json
{
  "status": "Success",
  "message": {
    "accessToken": "Bearer eyJhbGciOiJIUzI1NiIs..."
  }
}

Errors:

StatusCodeMessage
401UNAUTHORIZEDRefresh token is required
401UNAUTHORIZEDInvalid refresh token
401UNAUTHORIZEDRefresh token not found or revoked
401UNAUTHORIZEDRefresh token expired
401UNAUTHORIZEDUser not found or inactive

4.2 Get User Info

Get the current authenticated user's information.

Endpoint: GET /api/miniapp/api/user/info

Authentication: Access token required

Request:

http
GET /api/miniapp/api/user/info HTTP/1.1
Host: api.vio.com
Authorization: Bearer {accessToken}

Response:

json
{
  "status": "Success",
  "message": {
    "userId": "65f1a2b3c4d5e6f7a8b9c0d1",
    "username": "John Doe",
    "email": "john@example.com",
    "phoneNumber": "+66812345678",
    "walletAddr": "0x1234567890abcdef1234567890abcdef12345678",
    "tenantId": "65f1a2b3c4d5e6f7a8b9c0d0"
  }
}

Errors:

StatusCodeMessage
401UNAUTHORIZEDNo token provided
401UNAUTHORIZEDInvalid token
404NOT_FOUNDUser not found

4.3 List Tokens

Get the list of tokens available in the tenant, including the user's balances.

Endpoint: GET /api/miniapp/api/token/list

Authentication: Access token required

Query Parameters:

ParameterTypeDefaultDescription
pagenumber1Page number
sizenumber10Items per page

Response:

json
{
  "status": "Success",
  "message": {
    "page": 1,
    "size": 10,
    "totalCount": 2,
    "lastPage": 1,
    "data": [
      {
        "company": "65f1a2b3c4d5e6f7a8b9c0d0",
        "tokenName": "Acme Points",
        "tokenSymbol": "ACME",
        "balance": 1500,
        "dbTokId": "65f1a2b3c4d5e6f7a8b9c0d2",
        "decimal": 0,
        "logo": {
          "imageUrl": "https://cdn.vio.com/tokens/acme.png",
          "imageName": "ACME.png"
        }
      }
    ]
  }
}

Field Descriptions:

FieldDescription
companyTenant ID
tokenNameDisplay name of the token
tokenSymbolToken symbol (e.g., "ACME")
balanceUser's current balance
dbTokIdToken ID (use this for sending tokens)
decimalDecimal places for the token
logoToken logo image info (or null)

4.4 List Published Vouchers

Get the list of published voucher schemas available to the user.

Important: This endpoint only returns vouchers that have been added to active campaigns. Vouchers not linked to any campaign will not be returned. This matches the behavior of the member app.

Endpoint: GET /api/miniapp/api/campaign/voucherSchema/published/list

Authentication: Access token required

Query Parameters:

ParameterTypeDefaultDescription
pagenumber1Page number
sizenumber10Items per page

Filtering Logic:

  1. Campaign Filter: Only vouchers linked to active campaigns (via CampaignVoucher) are returned
  2. Organization Scope: Users see vouchers from campaigns at their organization level:
    • Tenant-level users see vouchers from tenant-level campaigns only
    • Sub-company users see vouchers from their sub-company's campaigns only
  3. Date Range: Only vouchers and campaigns within their active date range
  4. Visibility: Public vouchers, private vouchers from user's org, and shared vouchers

Response:

json
{
  "status": "Success",
  "message": {
    "page": 1,
    "size": 10,
    "totalCount": 1,
    "lastPage": 1,
    "data": [
      {
        "voucherSchema": {
          "voucherSchemaName": { "en": "50% Off Coffee" },
          "walletAddr": "0xabcdef1234567890...",
          "image": {
            "imageUrl": "https://cdn.vio.com/vouchers/coffee.jpg",
            "imageName": "50% Off Coffee.jpg"
          },
          "expiryStartDatetime": "2024-01-01T00:00:00.000Z",
          "expiryEndDatetime": "2024-12-31T23:59:59.000Z",
          "value": 50,
          "price": { "$numberDecimal": "100" },
          "isActive": true,
          "accessType": "Public Voucher",
          "maxPurchasePerUser": 5,
          "status": "Published",
          "isBookingRequired": false,
          "isLimited": true,
          "quantity": 1000,
          "voucherSchemaId": "65f1a2b3c4d5e6f7a8b9c0d4"
        },
        "isMaxQuantity": false,
        "status": "Published",
        "isActive": true,
        "campaignVoucherSchemaId": "65f1a2b3c4d5e6f7a8b9c0d4"
      }
    ]
  }
}

Key Fields:

FieldDescription
campaignVoucherSchemaIdUse this ID to issue vouchers to users
voucherSchemaName.enVoucher name in English
valueVoucher value (discount amount)
price.$numberDecimalToken cost to claim this voucher (from campaign configuration)
isMaxQuantityTrue if voucher is sold out
maxPurchasePerUserMax claims allowed per user
isLimitedWhether quantity is limited
quantityTotal quantity available

Note: If no vouchers are returned, ensure that:

  1. Vouchers have been added to at least one active campaign
  2. The campaign is active and within its date range
  3. The user has visibility access to the vouchers

4.5 Send Tokens to Users (Admin Only)

Send tokens to one or more users. Requires admin role (tenant_admin, sub_company_admin, or super_admin).

Endpoint: POST /api/miniapp/api/userManagement/token/send

Authentication: Access token required (admin role)

Request Body:

FieldTypeRequiredDescription
tokenstringYesToken ID (dbTokId from token list)
sendAmountnumberYesAmount to send to each user
userListstring[]YesArray of user IDs to receive tokens

Request:

json
{
  "token": "65f1a2b3c4d5e6f7a8b9c0d2",
  "sendAmount": 100,
  "userList": ["65f1a2b3c4d5e6f7a8b9c0d1"]
}

Response:

json
{
  "status": "Success",
  "message": {
    "totalUsers": 1,
    "successCount": 1,
    "failedCount": 0,
    "results": [
      {
        "userId": "65f1a2b3c4d5e6f7a8b9c0d1",
        "success": true,
        "newBalance": "1600"
      }
    ]
  }
}

Errors:

StatusCodeMessage
400VALIDATION_ERRORToken ID (dbTokId) is required
400VALIDATION_ERRORSend amount must be greater than 0
400VALIDATION_ERRORUser list must be a non-empty array
403FORBIDDENNot authorized (requires admin role)
403FORBIDDENToken does not belong to this tenant
404NOT_FOUNDToken not found

4.6 Send Voucher to User (Admin Only)

Issue a voucher to a specific user. Requires admin role (tenant_admin, sub_company_admin, or super_admin).

Endpoint: POST /api/miniapp/api/campaign/voucherSchema/send

Authentication: Access token required (admin role)

Request Body:

FieldTypeRequiredDescription
campaignVoucherSchemaIdstringYesVoucher schema ID (from voucher list)
userIdstringYesUser ID to receive the voucher

Request:

json
{
  "campaignVoucherSchemaId": "65f1a2b3c4d5e6f7a8b9c0d4",
  "userId": "65f1a2b3c4d5e6f7a8b9c0d1"
}

Response:

json
{
  "status": "Success",
  "message": {
    "voucherNftId": "65f1a2b3c4d5e6f7a8b9c0d6"
  }
}

TIP

After receiving the voucherNftId, you can redirect the user to view their voucher:

{redirectUrl}/voucher/{voucherNftId}

Errors:

StatusCodeMessage
400VALIDATION_ERRORCampaign voucher schema ID is required
400VALIDATION_ERRORUser ID is required
400VALIDATION_ERRORVoucher is not active
400VALIDATION_ERRORVoucher is not yet available
400VALIDATION_ERRORVoucher has expired
400VALIDATION_ERRORVoucher is sold out
400VALIDATION_ERRORUser has reached the maximum claim limit for this voucher
403FORBIDDENNot authorized (requires admin role)
404NOT_FOUNDVoucher not found
404NOT_FOUNDUser not found

4.7 Get User's Voucher NFT List

Get the list of vouchers claimed by the current user.

Endpoint: GET /api/miniapp/api/user/voucherNft/list

Authentication: Access token required

Query Parameters:

ParameterTypeDefaultDescription
pagenumber1Page number
sizenumber10Items per page

Response:

json
{
  "status": "Success",
  "message": {
    "page": 1,
    "size": 10,
    "totalCount": 1,
    "lastPage": 1,
    "data": [
      {
        "voucherNftId": "65f1a2b3c4d5e6f7a8b9c0d6",
        "nftTokenId": "123",
        "status": "active",
        "claimedAt": "2024-03-15T10:30:00.000Z",
        "redeemedAt": null,
        "expiresAt": "2024-12-31T23:59:59.000Z",
        "redemptionCode": "ABC12345",
        "voucherSchema": {
          "voucherSchemaName": { "en": "50% Off Coffee" },
          "voucherSchemaId": "65f1a2b3c4d5e6f7a8b9c0d4",
          "image": {
            "imageUrl": "https://cdn.vio.com/vouchers/coffee.jpg",
            "imageName": "50% Off Coffee.jpg"
          },
          "value": 50,
          "valueType": "percentage",
          "valueCurrency": "THB"
        }
      }
    ]
  }
}

Voucher Status Values:

StatusDescription
activeVoucher is available for use
usedVoucher has been redeemed
expiredVoucher has expired
transferredVoucher was transferred to another user

4.8 Health Check

Check if the Mini App API is running.

Endpoint: GET /api/miniapp/health

Authentication: None required

Response:

json
{
  "status": "Success",
  "message": {
    "service": "VIO MiniApp API",
    "version": "1.0.0",
    "timestamp": "2024-03-15T10:30:00.000Z"
  }
}

4.9 Public APIs (No Authentication Required)

These public endpoints are designed for mini apps that need to perform initial setup operations before user authentication, such as PIN verification for store-based interactions.

Base URL: https://api.vio.com/api/public


4.9.1 Verify PIN

Verify a redemption PIN and get the associated tenant information. This is useful for mini apps that need to identify which tenant a PIN belongs to before proceeding with further operations.

Endpoint: POST /api/public/verify-pin

Authentication: None required

Request Body:

FieldTypeRequiredDescription
pinstringYesFull PIN including prefix (e.g., "VI1234")

Request:

json
{
  "pin": "VI1234"
}

Response (Success):

json
{
  "success": true,
  "data": {
    "tenantId": "65f1a2b3c4d5e6f7a8b9c0d0",
    "tenantName": "Acme Corp",
    "tenantSlug": "acme",
    "message": "PIN verified successfully"
  }
}

How PIN Verification Works:

  1. The PIN format is {PREFIX}{4-digit-code} (e.g., VI1234)
  2. Each tenant has a unique 2-letter prefix (e.g., VI, AC, XY)
  3. The API extracts the prefix to identify the tenant
  4. The 4-digit code is verified against the tenant's active redemption PINs

Errors:

StatusMessage
400Invalid PIN format
400Invalid PIN prefix
400Invalid PIN
400No tenants configured with PIN prefixes

4.9.2 Get Stores by Tenant

Get a list of stores for a specific tenant. Useful for displaying store locations in a mini app after PIN verification.

Endpoint: GET /api/public/tenants/:tenantId/stores

Authentication: None required

Path Parameters:

ParameterTypeDescription
tenantIdstringThe tenant's MongoDB ObjectId

Query Parameters:

ParameterTypeDefaultDescription
pagenumber1Page number
limitnumber20Items per page (max: 100)
isActivestring-Filter by active status (true or false)

Request:

http
GET /api/public/tenants/65f1a2b3c4d5e6f7a8b9c0d0/stores?page=1&limit=20&isActive=true

Response:

json
{
  "success": true,
  "data": [
    {
      "id": "65f1a2b3c4d5e6f7a8b9c0e1",
      "name": "Downtown Store",
      "address": "123 Main Street, Bangkok 10110",
      "storeCode": "DTS001",
      "isActive": true,
      "latitude": 13.7563,
      "longitude": 100.5018,
      "createdAt": "2024-01-15T10:30:00.000Z"
    },
    {
      "id": "65f1a2b3c4d5e6f7a8b9c0e2",
      "name": "Mall Branch",
      "address": "456 Shopping Ave, Bangkok 10120",
      "storeCode": "MLB002",
      "isActive": true,
      "latitude": 13.7465,
      "longitude": 100.5392,
      "createdAt": "2024-02-20T14:00:00.000Z"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 2,
    "totalPages": 1
  }
}

Errors:

StatusMessage
404Tenant not found

4.9.3 Get Tenant Info

Get basic public information about a tenant, including branding details.

Endpoint: GET /api/public/tenants/:tenantId/info

Authentication: None required

Path Parameters:

ParameterTypeDescription
tenantIdstringThe tenant's MongoDB ObjectId

Request:

http
GET /api/public/tenants/65f1a2b3c4d5e6f7a8b9c0d0/info

Response:

json
{
  "success": true,
  "data": {
    "id": "65f1a2b3c4d5e6f7a8b9c0d0",
    "name": "Acme Corp",
    "slug": "acme",
    "branding": {
      "logo": "https://cdn.vio.com/tenants/acme/logo.png",
      "logoLight": "https://cdn.vio.com/tenants/acme/logo-light.png",
      "primaryColor": "#7C3AED",
      "secondaryColor": "#A78BFA"
    }
  }
}

Errors:

StatusMessage
404Tenant not found

Example: PIN Verification Flow

A typical mini app flow using these public APIs:

javascript
const API_BASE = 'https://api.vio.com';

// Step 1: User enters PIN (e.g., from scanning QR code)
const pin = 'VI1234';

// Step 2: Verify PIN and get tenant info
const verifyResponse = await fetch(`${API_BASE}/api/public/verify-pin`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ pin })
});

if (!verifyResponse.ok) {
  const error = await verifyResponse.json();
  console.error('PIN verification failed:', error.error.message);
  return;
}

const { data: pinData } = await verifyResponse.json();
console.log('Tenant identified:', pinData.tenantName);

// Step 3: Get tenant's stores
const storesResponse = await fetch(
  `${API_BASE}/api/public/tenants/${pinData.tenantId}/stores?isActive=true`
);
const { data: stores } = await storesResponse.json();

// Step 4: Display stores to user for selection
stores.forEach(store => {
  console.log(`${store.name} - ${store.address}`);
});

// Step 5: Get tenant branding for UI customization
const infoResponse = await fetch(
  `${API_BASE}/api/public/tenants/${pinData.tenantId}/info`
);
const { data: tenantInfo } = await infoResponse.json();

// Apply tenant branding
document.body.style.setProperty('--primary-color', tenantInfo.branding.primaryColor);

5. Error Handling

Error Response Format

All errors return this format:

json
{
  "success": false,
  "error": {
    "code": "ERROR_CODE",
    "message": "Human-readable error message"
  }
}

HTTP Status Codes

StatusDescription
400Bad Request - Validation error or invalid input
401Unauthorized - Missing or invalid token
403Forbidden - Insufficient permissions
404Not Found - Resource doesn't exist
409Conflict - Resource already exists
500Internal Server Error - Server-side error

Error Codes

CodeHTTP StatusDescription
VALIDATION_ERROR400Request validation failed
UNAUTHORIZED401Authentication required or failed
INVALID_TOKEN401JWT token is malformed
TOKEN_EXPIRED401JWT token has expired
FORBIDDEN403User lacks required permissions
NOT_FOUND404Requested resource not found
CONFLICT409Resource conflict (e.g., duplicate)
INTERNAL_ERROR500Internal server error

Handling Token Expiration

Access tokens expire after 15 minutes. When you receive a TOKEN_EXPIRED error, exchange your refresh token for a new access token:

javascript
async function apiCall(url, options = {}) {
  try {
    const response = await fetch(url, {
      ...options,
      headers: {
        ...options.headers,
        'Authorization': accessToken
      }
    });
    
    if (response.status === 401) {
      // Token might be expired, try to refresh
      await refreshAccessToken();
      // Retry the request
      return apiCall(url, options);
    }
    
    return response.json();
  } catch (error) {
    console.error('API call failed:', error);
    throw error;
  }
}

6. Redirect Back to VIO

Using the redirectUrl Parameter

The redirectUrl parameter contains the base URL for the VIO Member App (e.g., https://app.vio.com/acme). Use this to navigate users back to the VIO app.

Common Redirect Patterns

Redirect to home:

javascript
window.top.location.href = redirectUrl;

Redirect to a specific voucher:

javascript
const voucherNftId = "65f1a2b3c4d5e6f7a8b9c0d6";
window.top.location.href = `${redirectUrl}/voucher/${voucherNftId}`;

Redirect to voucher list:

javascript
window.top.location.href = `${redirectUrl}/vouchers`;

Important

Use window.top.location.href (not window.location.href) since your app runs in an iframe. The redirectUrl already includes the tenant slug.


7. Security Considerations

Token Handling

  1. Never expose tokens in logs or error messages
javascript
// Bad
console.log('Token:', accessToken);

// Good
console.log('Token received');
  1. Store tokens securely
javascript
// Use sessionStorage (cleared when tab closes)
sessionStorage.setItem('vio_access_token', accessToken);

// Avoid localStorage for sensitive tokens
  1. Clear tokens on logout or error
javascript
function clearTokens() {
  sessionStorage.removeItem('vio_access_token');
  sessionStorage.removeItem('vio_refresh_token');
}

CORS Considerations

The VIO API allows cross-origin requests from mini apps. Ensure your requests include:

javascript
fetch(url, {
  method: 'GET',
  headers: {
    'Authorization': `Bearer ${accessToken}`,
    'Content-Type': 'application/json'
  }
});

Iframe Security

Your mini app runs with these sandbox restrictions:

  • Cannot access the parent frame's DOM
  • Cannot navigate the parent frame (except via window.top.location.href)
  • Cannot access cookies from the parent domain

Input Validation

Always validate and sanitize any data before sending to the API:

javascript
function validateUserId(userId) {
  // MongoDB ObjectId format: 24 hex characters
  return /^[0-9a-fA-F]{24}$/.test(userId);
}

8. Getting Started / Quickstart

Vanilla JavaScript Example

Click to expand the full HTML example
html
<!DOCTYPE html>
<html>
<head>
  <title>VIO Mini App</title>
  <style>
    body { font-family: Arial, sans-serif; padding: 20px; }
    .user-info { background: #f5f5f5; padding: 15px; border-radius: 8px; }
    .token-card { border: 1px solid #ddd; padding: 10px; margin: 10px 0; border-radius: 4px; }
    .loading { color: #666; }
    .error { color: #d32f2f; }
  </style>
</head>
<body>
  <h1>My VIO Mini App</h1>
  <div id="content">
    <p class="loading">Loading...</p>
  </div>

  <script>
    const API_BASE = 'https://api.vio.com/api/miniapp';
    
    let accessToken = null;
    let refreshToken = null;
    let redirectUrl = null;

    function getUrlParams() {
      const params = new URLSearchParams(window.location.search);
      return {
        tenant: params.get('tenant'),
        userId: params.get('userId'),
        refreshToken: params.get('refreshToken'),
        redirectUrl: params.get('redirectUrl')
      };
    }

    async function exchangeToken(refreshToken) {
      const response = await fetch(`${API_BASE}/api/auth/token`, {
        method: 'GET',
        headers: { 'Authorization': refreshToken }
      });
      if (!response.ok) throw new Error('Failed to exchange token');
      const data = await response.json();
      return data.message.accessToken;
    }

    async function getUserInfo() {
      const response = await fetch(`${API_BASE}/api/user/info`, {
        headers: { 'Authorization': accessToken }
      });
      if (!response.ok) throw new Error('Failed to get user info');
      const data = await response.json();
      return data.message;
    }

    async function getTokenList() {
      const response = await fetch(`${API_BASE}/api/token/list?page=1&size=10`, {
        headers: { 'Authorization': accessToken }
      });
      if (!response.ok) throw new Error('Failed to get tokens');
      const data = await response.json();
      return data.message.data;
    }

    async function init() {
      const content = document.getElementById('content');
      try {
        const params = getUrlParams();
        if (!params.refreshToken) throw new Error('No refresh token provided');
        
        refreshToken = params.refreshToken;
        redirectUrl = params.redirectUrl;
        
        accessToken = await exchangeToken(refreshToken);
        
        const [userInfo, tokens] = await Promise.all([
          getUserInfo(),
          getTokenList()
        ]);
        
        content.innerHTML = `
          <div class="user-info">
            <h3>Welcome, ${userInfo.username || 'User'}!</h3>
            <p>Email: ${userInfo.email || 'N/A'}</p>
          </div>
          <h3>Your Tokens</h3>
          ${tokens.map(t => `
            <div class="token-card">
              <strong>${t.tokenName}</strong> (${t.tokenSymbol})<br>
              Balance: ${t.balance}
            </div>
          `).join('')}
          <button onclick="window.top.location.href='${redirectUrl}'">Back to VIO</button>
        `;
      } catch (error) {
        content.innerHTML = `<p class="error">Error: ${error.message}</p>`;
      }
    }

    init();
  </script>
</body>
</html>

React Example

jsx
import { useState, useEffect } from 'react';

const API_BASE = 'https://api.vio.com/api/miniapp';

function App() {
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [accessToken, setAccessToken] = useState(null);
  const [user, setUser] = useState(null);
  const [tokens, setTokens] = useState([]);
  const [redirectUrl, setRedirectUrl] = useState(null);

  useEffect(() => {
    init();
  }, []);

  async function init() {
    try {
      const params = new URLSearchParams(window.location.search);
      const refreshToken = params.get('refreshToken');
      const redirect = params.get('redirectUrl');
      setRedirectUrl(redirect);

      if (!refreshToken) throw new Error('No refresh token provided');

      // Exchange token
      const tokenResponse = await fetch(`${API_BASE}/api/auth/token`, {
        headers: { 'Authorization': refreshToken }
      });
      if (!tokenResponse.ok) throw new Error('Token exchange failed');
      const tokenData = await tokenResponse.json();
      const token = tokenData.message.accessToken;
      setAccessToken(token);

      // Fetch data
      const [userRes, tokensRes] = await Promise.all([
        fetch(`${API_BASE}/api/user/info`, { headers: { 'Authorization': token } }),
        fetch(`${API_BASE}/api/token/list?page=1&size=10`, { headers: { 'Authorization': token } })
      ]);

      if (!userRes.ok || !tokensRes.ok) throw new Error('Failed to fetch data');

      const userData = await userRes.json();
      const tokensData = await tokensRes.json();

      setUser(userData.message);
      setTokens(tokensData.message.data);
      setLoading(false);
    } catch (err) {
      setError(err.message);
      setLoading(false);
    }
  }

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;

  return (
    <div className="app">
      <h1>My VIO Mini App</h1>
      {user && (
        <div>
          <h2>Welcome, {user.username || 'User'}!</h2>
          <p>Email: {user.email || 'N/A'}</p>
        </div>
      )}
      <h3>Your Tokens</h3>
      {tokens.map(token => (
        <div key={token.dbTokId}>
          <strong>{token.tokenName}</strong> ({token.tokenSymbol})
          - Balance: {token.balance}
        </div>
      ))}
      <button onClick={() => window.top.location.href = redirectUrl}>
        Back to VIO
      </button>
    </div>
  );
}

export default App;
jsx
import { useState, useCallback } from 'react';

const API_BASE = 'https://api.vio.com/api/miniapp';

export function useVioApi() {
  const [accessToken, setAccessToken] = useState(null);

  const exchangeToken = useCallback(async (refreshToken) => {
    const response = await fetch(`${API_BASE}/api/auth/token`, {
      headers: { 'Authorization': refreshToken }
    });
    if (!response.ok) throw new Error('Token exchange failed');
    const data = await response.json();
    const token = data.message.accessToken;
    setAccessToken(token);
    return token;
  }, []);

  const apiCall = useCallback(async (endpoint, options = {}) => {
    if (!accessToken) throw new Error('Not authenticated');
    const response = await fetch(`${API_BASE}${endpoint}`, {
      ...options,
      headers: {
        ...options.headers,
        'Authorization': accessToken,
        'Content-Type': 'application/json'
      }
    });
    const data = await response.json();
    if (!response.ok) throw new Error(data.error?.message || 'API call failed');
    return data.message;
  }, [accessToken]);

  return {
    accessToken,
    exchangeToken,
    getUserInfo: () => apiCall('/api/user/info'),
    getTokenList: (page = 1, size = 10) =>
      apiCall(`/api/token/list?page=${page}&size=${size}`),
    getVoucherList: (page = 1, size = 10) =>
      apiCall(`/api/campaign/voucherSchema/published/list?page=${page}&size=${size}`),
    getUserVouchers: (page = 1, size = 10) =>
      apiCall(`/api/user/voucherNft/list?page=${page}&size=${size}`),
    sendTokens: (tokenId, amount, userIds) =>
      apiCall('/api/userManagement/token/send', {
        method: 'POST',
        body: JSON.stringify({ token: tokenId, sendAmount: amount, userList: userIds })
      }),
    sendVoucher: (voucherId, userId) =>
      apiCall('/api/campaign/voucherSchema/send', {
        method: 'POST',
        body: JSON.stringify({ campaignVoucherSchemaId: voucherId, userId })
      })
  };
}

9. Registration with VIO

How to Get Your Mini App Added

To have your mini app appear in the VIO Member App, coordinate with the VIO platform administrator (Super Admin). They will configure your mini app in the tenant's home configuration.

Configuration Fields

When requesting to add your mini app, provide these details:

FieldRequiredDescription
labelYesDisplay name shown under the icon (max 20 chars)
iconYesLucide icon name (e.g., "Gift", "Star", "Ticket")
miniAppUrlYesYour mini app URL for the member-facing app
miniAppAdminUrlNoYour mini app URL for the admin portal (if different)

Available Icon Names

Common Lucide icons you can request:

  • Gift, Star, Ticket, Dices, Car, ShoppingCart
  • CreditCard, Wallet, Coins, Trophy, Medal
  • Calendar, Clock, Map, Navigation, Compass
  • Heart, ThumbsUp, Sparkles, Zap, Flame

Full list: https://lucide.dev/icons/

Mini App Placement Options

Your mini app can appear in:

  1. Home Page Grid - Up to 8 mini app icons in a 4-column grid
  2. Bottom Navigation Bar - As one of 3-5 navigation items
  3. Bottom Section - Full-width iframe in the home page bottom section

Example Request to VIO Admin

Please add our Lucky Draw mini app to Tenant: ACME

Configuration:
- Label: Lucky Draw
- Icon: Dices
- Member App URL: https://luckydraw.example.com
- Admin Portal URL: https://admin.luckydraw.example.com

Placement: Home page grid

Testing Your Integration

Before going live:

  1. Test locally using a development VIO environment
  2. Verify authentication works correctly
  3. Test all API endpoints you plan to use
  4. Check error handling for expired tokens, network failures
  5. Test redirect back to VIO functionality
  6. Verify mobile responsiveness (iframe is full-width on mobile)

Appendix: API Endpoint Summary

Authenticated Endpoints

MethodEndpointAuthDescription
GET/api/miniapp/api/auth/tokenRefresh TokenExchange refresh token for access token
GET/api/miniapp/api/user/infoAccess TokenGet current user info
GET/api/miniapp/api/token/listAccess TokenList tenant tokens with user balances
GET/api/miniapp/api/campaign/voucherSchema/published/listAccess TokenList vouchers from active campaigns
POST/api/miniapp/api/userManagement/token/sendAccess Token (Admin)Send tokens to users
POST/api/miniapp/api/campaign/voucherSchema/sendAccess Token (Admin)Issue voucher to user
GET/api/miniapp/api/user/voucherNft/listAccess TokenGet user's claimed vouchers
GET/api/miniapp/healthNoneHealth check

Public Endpoints (No Authentication)

MethodEndpointAuthDescription
POST/api/public/verify-pinNoneVerify PIN and get tenant info
GET/api/public/tenants/:tenantId/storesNoneGet stores list for a tenant
GET/api/public/tenants/:tenantId/infoNoneGet tenant public info and branding

Need Help?

For questions or support regarding mini app integration, contact the VIO platform team.

VIO v4 Platform Documentation