Appearance
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:
| Parameter | Type | Description |
|---|---|---|
tenant | string | The tenant slug (e.g., acme) |
company | string | Same as tenant slug |
userId | string | The current user's ID (MongoDB ObjectId) |
refreshToken | string | JWT refresh token prefixed with Bearer |
redirectUrl | string | Base 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%2FacmeIframe 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:
| Permission | Description |
|---|---|
allow-scripts | JavaScript execution is allowed |
allow-same-origin | Can access same-origin resources and storage |
allow-forms | Form submission is allowed |
allow-popups | Can open new windows/tabs |
allow-popups-to-escape-sandbox | Opened popups are not sandboxed |
geolocation | Can request user's location |
camera | Can access camera |
microphone | Can 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 Type | Lifetime | Usage |
|---|---|---|
| Refresh Token | 7 days | Exchange for access token |
| Access Token | 15 minutes | API 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:
| Status | Code | Message |
|---|---|---|
| 401 | UNAUTHORIZED | Refresh token is required |
| 401 | UNAUTHORIZED | Invalid refresh token |
| 401 | UNAUTHORIZED | Refresh token not found or revoked |
| 401 | UNAUTHORIZED | Refresh token expired |
| 401 | UNAUTHORIZED | User 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:
| Status | Code | Message |
|---|---|---|
| 401 | UNAUTHORIZED | No token provided |
| 401 | UNAUTHORIZED | Invalid token |
| 404 | NOT_FOUND | User 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:
| Parameter | Type | Default | Description |
|---|---|---|---|
page | number | 1 | Page number |
size | number | 10 | Items 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:
| Field | Description |
|---|---|
company | Tenant ID |
tokenName | Display name of the token |
tokenSymbol | Token symbol (e.g., "ACME") |
balance | User's current balance |
dbTokId | Token ID (use this for sending tokens) |
decimal | Decimal places for the token |
logo | Token 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:
| Parameter | Type | Default | Description |
|---|---|---|---|
page | number | 1 | Page number |
size | number | 10 | Items per page |
Filtering Logic:
- Campaign Filter: Only vouchers linked to active campaigns (via
CampaignVoucher) are returned - 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
- Date Range: Only vouchers and campaigns within their active date range
- 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:
| Field | Description |
|---|---|
campaignVoucherSchemaId | Use this ID to issue vouchers to users |
voucherSchemaName.en | Voucher name in English |
value | Voucher value (discount amount) |
price.$numberDecimal | Token cost to claim this voucher (from campaign configuration) |
isMaxQuantity | True if voucher is sold out |
maxPurchasePerUser | Max claims allowed per user |
isLimited | Whether quantity is limited |
quantity | Total quantity available |
Note: If no vouchers are returned, ensure that:
- Vouchers have been added to at least one active campaign
- The campaign is active and within its date range
- 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:
| Field | Type | Required | Description |
|---|---|---|---|
token | string | Yes | Token ID (dbTokId from token list) |
sendAmount | number | Yes | Amount to send to each user |
userList | string[] | Yes | Array 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:
| Status | Code | Message |
|---|---|---|
| 400 | VALIDATION_ERROR | Token ID (dbTokId) is required |
| 400 | VALIDATION_ERROR | Send amount must be greater than 0 |
| 400 | VALIDATION_ERROR | User list must be a non-empty array |
| 403 | FORBIDDEN | Not authorized (requires admin role) |
| 403 | FORBIDDEN | Token does not belong to this tenant |
| 404 | NOT_FOUND | Token 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:
| Field | Type | Required | Description |
|---|---|---|---|
campaignVoucherSchemaId | string | Yes | Voucher schema ID (from voucher list) |
userId | string | Yes | User 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:
| Status | Code | Message |
|---|---|---|
| 400 | VALIDATION_ERROR | Campaign voucher schema ID is required |
| 400 | VALIDATION_ERROR | User ID is required |
| 400 | VALIDATION_ERROR | Voucher is not active |
| 400 | VALIDATION_ERROR | Voucher is not yet available |
| 400 | VALIDATION_ERROR | Voucher has expired |
| 400 | VALIDATION_ERROR | Voucher is sold out |
| 400 | VALIDATION_ERROR | User has reached the maximum claim limit for this voucher |
| 403 | FORBIDDEN | Not authorized (requires admin role) |
| 404 | NOT_FOUND | Voucher not found |
| 404 | NOT_FOUND | User 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:
| Parameter | Type | Default | Description |
|---|---|---|---|
page | number | 1 | Page number |
size | number | 10 | Items 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:
| Status | Description |
|---|---|
active | Voucher is available for use |
used | Voucher has been redeemed |
expired | Voucher has expired |
transferred | Voucher 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:
| Field | Type | Required | Description |
|---|---|---|---|
pin | string | Yes | Full 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:
- The PIN format is
{PREFIX}{4-digit-code}(e.g.,VI1234) - Each tenant has a unique 2-letter prefix (e.g.,
VI,AC,XY) - The API extracts the prefix to identify the tenant
- The 4-digit code is verified against the tenant's active redemption PINs
Errors:
| Status | Message |
|---|---|
| 400 | Invalid PIN format |
| 400 | Invalid PIN prefix |
| 400 | Invalid PIN |
| 400 | No 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:
| Parameter | Type | Description |
|---|---|---|
tenantId | string | The tenant's MongoDB ObjectId |
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
page | number | 1 | Page number |
limit | number | 20 | Items per page (max: 100) |
isActive | string | - | Filter by active status (true or false) |
Request:
http
GET /api/public/tenants/65f1a2b3c4d5e6f7a8b9c0d0/stores?page=1&limit=20&isActive=trueResponse:
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:
| Status | Message |
|---|---|
| 404 | Tenant 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:
| Parameter | Type | Description |
|---|---|---|
tenantId | string | The tenant's MongoDB ObjectId |
Request:
http
GET /api/public/tenants/65f1a2b3c4d5e6f7a8b9c0d0/infoResponse:
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:
| Status | Message |
|---|---|
| 404 | Tenant 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
| Status | Description |
|---|---|
| 400 | Bad Request - Validation error or invalid input |
| 401 | Unauthorized - Missing or invalid token |
| 403 | Forbidden - Insufficient permissions |
| 404 | Not Found - Resource doesn't exist |
| 409 | Conflict - Resource already exists |
| 500 | Internal Server Error - Server-side error |
Error Codes
| Code | HTTP Status | Description |
|---|---|---|
VALIDATION_ERROR | 400 | Request validation failed |
UNAUTHORIZED | 401 | Authentication required or failed |
INVALID_TOKEN | 401 | JWT token is malformed |
TOKEN_EXPIRED | 401 | JWT token has expired |
FORBIDDEN | 403 | User lacks required permissions |
NOT_FOUND | 404 | Requested resource not found |
CONFLICT | 409 | Resource conflict (e.g., duplicate) |
INTERNAL_ERROR | 500 | Internal 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
- Never expose tokens in logs or error messages
javascript
// Bad
console.log('Token:', accessToken);
// Good
console.log('Token received');- Store tokens securely
javascript
// Use sessionStorage (cleared when tab closes)
sessionStorage.setItem('vio_access_token', accessToken);
// Avoid localStorage for sensitive tokens- 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:
| Field | Required | Description |
|---|---|---|
label | Yes | Display name shown under the icon (max 20 chars) |
icon | Yes | Lucide icon name (e.g., "Gift", "Star", "Ticket") |
miniAppUrl | Yes | Your mini app URL for the member-facing app |
miniAppAdminUrl | No | Your mini app URL for the admin portal (if different) |
Available Icon Names
Common Lucide icons you can request:
Gift,Star,Ticket,Dices,Car,ShoppingCartCreditCard,Wallet,Coins,Trophy,MedalCalendar,Clock,Map,Navigation,CompassHeart,ThumbsUp,Sparkles,Zap,Flame
Full list: https://lucide.dev/icons/
Mini App Placement Options
Your mini app can appear in:
- Home Page Grid - Up to 8 mini app icons in a 4-column grid
- Bottom Navigation Bar - As one of 3-5 navigation items
- 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 gridTesting Your Integration
Before going live:
- Test locally using a development VIO environment
- Verify authentication works correctly
- Test all API endpoints you plan to use
- Check error handling for expired tokens, network failures
- Test redirect back to VIO functionality
- Verify mobile responsiveness (iframe is full-width on mobile)
Appendix: API Endpoint Summary
Authenticated Endpoints
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /api/miniapp/api/auth/token | Refresh Token | Exchange refresh token for access token |
| GET | /api/miniapp/api/user/info | Access Token | Get current user info |
| GET | /api/miniapp/api/token/list | Access Token | List tenant tokens with user balances |
| GET | /api/miniapp/api/campaign/voucherSchema/published/list | Access Token | List vouchers from active campaigns |
| POST | /api/miniapp/api/userManagement/token/send | Access Token (Admin) | Send tokens to users |
| POST | /api/miniapp/api/campaign/voucherSchema/send | Access Token (Admin) | Issue voucher to user |
| GET | /api/miniapp/api/user/voucherNft/list | Access Token | Get user's claimed vouchers |
| GET | /api/miniapp/health | None | Health check |
Public Endpoints (No Authentication)
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| POST | /api/public/verify-pin | None | Verify PIN and get tenant info |
| GET | /api/public/tenants/:tenantId/stores | None | Get stores list for a tenant |
| GET | /api/public/tenants/:tenantId/info | None | Get tenant public info and branding |
Need Help?
For questions or support regarding mini app integration, contact the VIO platform team.