VIO External API Reference
This document provides a comprehensive reference for integrating with the VIO External API.
Table of Contents
1. Overview
What is the VIO External API?
The VIO External API allows tenants to integrate their systems with the VIO platform programmatically. Use this API to:
- Manage vouchers (create, update, delete, redeem)
- Manage users and their data
- Create and manage voucher campaigns
- Handle tokens, balances, and transactions
- Access analytics and reporting data
Base URL
https://{your-domain}/api/external/v1All API endpoints are relative to this base URL.
API Versioning
The current API release version is 1.4.0. The URL path version remains v1. When breaking changes are introduced, a new URL path version will be released while maintaining backwards compatibility for existing versions.
Architecture
┌─────────────────────────────────────────────────────────────────┐
│ Your Application │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 1. Include X-API-Key header │ │
│ │ 2. Make HTTPS request to /api/external/v1/* │ │
│ │ 3. Parse JSON response │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────┐
│ VIO API Server │
│ /api/external/v1 │
│ │
│ • Authentication │
│ • Rate Limiting │
│ • Scope Validation │
│ • Request Handler │
└─────────────────────┘
│
▼
┌─────────────────────┐
│ Database │
│ (Your Tenant Data) │
└─────────────────────┘2. Authentication
API Keys
All API requests require authentication using an API key. API keys are scoped to your tenant and can have different permission levels.
Obtaining an API Key
- Log in to the VIO Admin Portal
- Navigate to Settings > API Keys
- Click Create API Key
- Select the scopes (permissions) for the key
- Optionally configure IP whitelisting
- Copy and securely store the generated key
Important: API keys are shown only once upon creation. Store them securely.
Using Your API Key
Include your API key in the X-API-Key header with every request:
curl -X GET "https://your-domain.com/api/external/v1/vouchers" \
-H "X-API-Key: vio_live_your_api_key_here"API Key Format
API keys follow this format:
- Live keys:
vio_live_xxxxxxxxxxxxxxxx - Test keys:
vio_test_xxxxxxxxxxxxxxxx
API Key Scopes
Each API key can be assigned one or more scopes that determine which endpoints it can access:
| Scope | Description | Endpoints |
|---|---|---|
vouchers | Manage vouchers and claims | /vouchers/* |
users | Manage users | /users/* |
campaigns | Manage campaigns | /campaigns/* |
tokens | Manage tokens and balances | /tokens/* |
analytics | Access analytics data | /analytics/* |
IP Whitelisting
For additional security, you can restrict API key usage to specific IP addresses:
- Go to Admin Portal > Settings > API Keys
- Edit your API key
- Add allowed IP addresses or CIDR ranges
- Save changes
Requests from non-whitelisted IPs will receive a 403 Forbidden response.
3. Rate Limiting
Default Limits
To ensure fair usage and platform stability, the API enforces rate limits:
| Limit Type | Default Value |
|---|---|
| Per Minute | 60 requests |
| Per Day | 10,000 requests |
Rate Limit Headers
Every response includes rate limit information in the headers:
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests allowed in the current window |
X-RateLimit-Remaining | Requests remaining in the current window |
X-RateLimit-Reset | Unix timestamp when the rate limit resets |
Example Response Headers
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1699574400Rate Limit Exceeded
When you exceed the rate limit, you'll receive a 429 Too Many Requests response:
{
"success": false,
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Too many requests. Please retry after 60 seconds."
}
}The response includes a Retry-After header indicating how long to wait before retrying.
Best Practices
- Implement exponential backoff when receiving
429responses - Cache responses when appropriate
- Batch operations when possible
- Monitor your usage via the rate limit headers
4. Request & Response Format
Request Format
- All request bodies must be JSON
- Include
Content-Type: application/jsonheader for POST/PATCH requests - Query parameters are used for filtering and pagination
Success Response
Successful responses follow this structure:
{
"success": true,
"data": { ... },
"message": "Optional success message"
}Paginated Response
List endpoints return paginated data:
{
"success": true,
"data": [ ... ],
"pagination": {
"page": 1,
"limit": 20,
"total": 150,
"totalPages": 8,
"hasNextPage": true,
"hasPrevPage": false
}
}Pagination Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number (1-indexed) |
limit | integer | 20 | Items per page (max: 100) |
Error Response
Error responses follow this structure:
{
"success": false,
"error": {
"code": "ERROR_CODE",
"message": "Human-readable error description"
}
}5. Error Handling
HTTP Status Codes
| Status Code | Meaning |
|---|---|
200 OK | Request succeeded |
201 Created | Resource created successfully |
400 Bad Request | Invalid request parameters or body |
401 Unauthorized | Missing or invalid API key |
403 Forbidden | API key lacks required scope or IP not whitelisted |
404 Not Found | Resource not found |
429 Too Many Requests | Rate limit exceeded |
500 Internal Server Error | Server error |
Error Codes
| Error Code | Description |
|---|---|
VALIDATION_ERROR | Request body or parameters failed validation |
UNAUTHORIZED | API key is missing or invalid |
FORBIDDEN | API key does not have required scope |
NOT_FOUND | Requested resource does not exist |
RATE_LIMIT_EXCEEDED | Too many requests |
ALREADY_EXISTS | Resource already exists (e.g., duplicate user) |
INSUFFICIENT_BALANCE | Not enough token balance for operation |
ALREADY_REDEEMED | Voucher has already been redeemed |
EXPIRED | Voucher or token has expired |
INTERNAL_ERROR | Unexpected server error |
Example Error Response
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid request parameters",
"details": [
{
"field": "email",
"message": "Invalid email format"
}
]
}
}6. API Reference: Vouchers
Required Scope: vouchers
Claim & Redeem Flows (Important)
Treat these as two different objects:
- Voucher template (
Voucher): the voucher definition your tenant creates or syncs. - Voucher claim (
UserVoucher): the per-user voucher instance created by claim/send.
Voucher lifecycle by scenario
Use the following flow selection when integrating only through APIs:
| Scenario | Step 1 (issue/claim) | Step 2 (redeem) | When to use |
|---|---|---|---|
| User self-claim from campaign | POST /api/vouchers/:voucherId/claim with campaignId | POST /api/vouchers/me/:userVoucherId/redeem | End-user app (JWT) |
| Tenant/backend directly issues to user | POST /vouchers/:voucherId/send with userId | POST /vouchers/redeem-by-code or POST /vouchers/redeem/:code/pin | API Key server-to-server integration |
| Backend claims on behalf of user (with token deduction) | POST /vouchers/:voucherId/redeem-with-tokens with userId + campaignId | POST /vouchers/redeem-by-code or POST /vouchers/redeem/:code/pin | API Key — atomic token-to-voucher exchange |
The flow is the same for both tenant-managed and external vouchers: claim/send first, then redeem. The difference is what happens at redeem time:
- Tenant-managed voucher: redeem marks the claim as used; no external action is triggered. For
consumptionTypeqr_codeorcoupon_codewith a supplier code pool (CSV uploaded in Admin Portal), redeem assigns the next available code toexternalVoucherCodeand then marks the claim used — only then should end-user apps show that supplier code / generated QR or barcode. - External voucher: redeem triggers a provider purchase. After redeem, check
externalFulfillmentStatusin the response to confirm success.
POST /vouchers/:voucherId/send is the External API equivalent of "claim for a specific user". It creates a UserVoucher and returns a redemptionCode.
Voucher type decision rules
Determine voucher source/type from voucher template fields:
externalProviderandexternalIdboth present: this is an external voucher.externalProviderandexternalIdboth missing: this is a tenant-managed voucher.consumptionTypeis usage mode (vio_code,coupon_code,url,qr_code,manual,zhichong), not source-of-truth for tenant/external.voucherTypeis business category, not source-of-truth for tenant/external.
Where to read decision fields
| What you need to decide | Read from API | Key fields |
|---|---|---|
| Is this voucher external or tenant-managed? | GET /vouchers or GET /vouchers/:voucherId | externalProvider, externalId |
| Which redeem input style is needed? | GET /vouchers or GET /vouchers/:voucherId | consumptionType, externalRequiresDirectOrderParams |
| Which claim instance should be redeemed? | Claim/send response or user-claim list | userVoucherId (_id), redemptionCode, status |
| External fulfillment outputs after redeem | Redeem response | externalVoucherCode, externalRedemptionUrl, externalOrderId, externalFulfillmentStatus |
GET /vouchers/redeem/:code/info is display-oriented and should not be used as the voucher type decision source.
Claim/Redeem Request & Response Examples
A) Directly issue (send) a voucher to a user via External API (API Key)
This is the correct endpoint when your backend issues vouchers directly to users.
curl -X POST "https://your-domain.com/api/external/v1/vouchers/{voucherId}/send" \
-H "X-API-Key: vio_live_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"userId": "507f1f77bcf86cd799439015"
}'Example success response (key fields):
{
"success": true,
"data": {
"_id": "507f1f77bcf86cd799439050",
"voucherId": "507f1f77bcf86cd799439011",
"userId": "507f1f77bcf86cd799439015",
"status": "active",
"redemptionCode": "VCH-M1ABC2-XY3Z"
},
"message": "Voucher sent to user"
}B) Redeem by redemption code via External API (API Key)
Suitable for non-zhichong server-to-server/store integrations.
curl -X POST "https://your-domain.com/api/external/v1/vouchers/redeem-by-code" \
-H "X-API-Key: vio_live_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"redemptionCode": "VCH-M1ABC2-XY3Z",
"location": "Store #42",
"notes": "POS order #12345"
}'Example success response (key fields):
{
"success": true,
"data": {
"_id": "507f1f77bcf86cd799439050",
"status": "redeemed",
"redemptionCode": "VCH-M1ABC2-XY3Z",
"redeemedAt": "2024-02-01T10:15:00.000Z",
"externalVoucherCode": "ABC-123-XYZ",
"externalRedemptionUrl": null,
"externalOrderId": "69f75fe31134f32e472b2f98",
"externalFulfillmentStatus": "fulfilled"
},
"message": "Voucher redeemed"
}C) zhichong direct recharge redeem (Member API, JWT)
When consumptionType is zhichong, redeem through member endpoint and include recharge account:
curl -X POST "https://your-domain.com/api/vouchers/me/{userVoucherId}/redeem" \
-H "Authorization: Bearer {member_jwt}" \
-H "Content-Type: application/json" \
-d '{
"providerParams": {
"account": "12312332123"
}
}'Example success response (key fields):
{
"success": true,
"data": {
"_id": "507f1f77bcf86cd799439050",
"status": "redeemed",
"externalFulfillmentStatus": "fulfilled",
"externalIsDirectRecharge": true,
"externalRechargeAccount": "12312332123",
"externalOrderId": "69f75fe31134f32e472b2f98"
},
"message": "Voucher redeemed"
}Voucher Source & Type Matrix
| Voucher source | externalProvider | externalId | Typical consumptionType | Recommended claim/send | Recommended redeem |
|---|---|---|---|---|---|
| Tenant-managed voucher | missing | missing | vio_code, coupon_code, url, qr_code, manual | POST /vouchers/:voucherId/send (API Key) or member claim flow | POST /vouchers/redeem-by-code, POST /vouchers/redeem/:code/pin, or member redeem |
| External voucher (code/url style) | present | present | usually coupon_code or url | Claim/send first | Redeem after claim/send |
External voucher (zhichong) | present | present | zhichong | Claim/send first | POST /api/vouchers/me/:userVoucherId/redeem with providerParams.account |
POST /api/external/v1/vouchers/redeem-by-codedoes not acceptproviderParams.POST /api/external/v1/vouchers/redeem/:code/pincannot be used forzhichong.
Error handling & fulfillment status
Common claim/send errors
| HTTP Status | Error Code | Cause |
|---|---|---|
| 404 | NOT_FOUND | voucherId or userId does not exist |
| 400 | VALIDATION_ERROR | Voucher is not active or has expired |
| 400 | VALIDATION_ERROR | Voucher is sold out (claimedQuantity >= totalQuantity) |
| 400 | VALIDATION_ERROR | User has reached maxClaimsPerUser limit |
Common redeem errors
| HTTP Status | Error Code | Cause |
|---|---|---|
| 404 | NOT_FOUND | redemptionCode does not match any active claim |
| 400 | VALIDATION_ERROR | Voucher claim is already redeemed (ALREADY_REDEEMED) |
| 400 | VALIDATION_ERROR | Voucher claim has expired (EXPIRED) |
| 400 | VALIDATION_ERROR | zhichong voucher cannot use PIN redeem |
External voucher fulfillment status
After redeeming an external voucher, inspect externalFulfillmentStatus in the response:
externalFulfillmentStatus | Meaning | Action required |
|---|---|---|
fulfilled | Provider purchase succeeded. externalVoucherCode or externalRedemptionUrl is available (for non-zhichong) | Deliver the code/url to the user |
failed | Provider purchase failed | You may retry by calling redeem again on the same claim. Check externalFulfillmentError for details |
processing | Provider purchase is in progress (rare, async scenarios) | Retry later or poll the claim status via GET /users/:userId/vouchers |
If the provider purchase fails, the redeem request returns an error and the claim remains retryable. The claim record is updated with externalFulfillmentStatus: "failed" and externalFulfillmentError.
Example claim state after a failed external fulfillment:
{
"_id": "507f1f77bcf86cd799439050",
"status": "active",
"externalFulfillmentStatus": "failed",
"externalFulfillmentError": "Provider returned: insufficient inventory",
"externalOrderId": null
}When externalFulfillmentStatus is failed, the claim remains in active status and can be retried by calling redeem again.
List Vouchers
Retrieve a paginated list of vouchers visible to your tenant. You can optionally exclude vouchers manually created by the current tenant.
GET /vouchersQuery Parameters:
| Parameter | Type | Required | Constraints | Description |
|---|---|---|---|---|
page | integer | No | Min: 1. Default: 1 | Page number (1-indexed) |
limit | integer | No | Min: 1, Max: 100. Default: 20 | Items per page |
visibility | string | No | Enum: private, public, shared | Filter by visibility scope |
isActive | boolean | No | String "true" or "false" (parsed as boolean) | Filter by active status |
category | string | No | Free-form string | Filter by category tag |
search | string | No | Free-form string | Search by voucher name (partial match) |
subCompanyId | string | No | MongoDB ObjectId (24 hex chars) | Filter by sub-company |
excludeTenantManualCreated | boolean | No | String "true" or "false". Default: false | When true, exclude vouchers manually created by the current tenant while keeping externally supplied vouchers such as vouchain |
Example Request:
curl -X GET "https://your-domain.com/api/external/v1/vouchers?page=1&limit=10&isActive=true&excludeTenantManualCreated=true" \
-H "X-API-Key: vio_live_your_api_key_here"Example Response:
{
"success": true,
"data": [
{
"_id": "507f1f77bcf86cd799439011",
"name": "20% Off Discount",
"description": "Get 20% off your next purchase",
"value": 20,
"valueType": "percentage",
"terms": "Valid on orders over $50",
"images": ["https://example.com/image.jpg"],
"isActive": true,
"totalQuantity": 100,
"claimedQuantity": 45,
"maxClaimsPerUser": 1,
"voucherType": "discount",
"consumptionType": "coupon_code",
"externalProvider": "vouchain",
"externalId": "VCH-EXT-001",
"externalRequiresDirectOrderParams": false,
"category": "Lifestyle & Services",
"categories": ["food", "lifestyle"],
"settlementAmount": 80,
"settlementCurrency": "THB",
"startDate": "2024-01-01T00:00:00.000Z",
"endDate": "2024-12-31T23:59:59.000Z",
"visibility": "public",
"createdAt": "2024-01-01T00:00:00.000Z",
"updatedAt": "2024-01-15T10:30:00.000Z"
}
],
"pagination": {
"page": 1,
"limit": 10,
"total": 45,
"totalPages": 5,
"hasNextPage": true,
"hasPrevPage": false
}
}Response Fields (each item in data array):
| Field | Type | Nullable | Constraints / Format | Description |
|---|---|---|---|---|
_id | string | No | MongoDB ObjectId (24 hex chars) | Unique identifier for the voucher |
name | string | No | Min: 1 char. Trimmed | Display name of the voucher |
description | string | No | May be empty string "" | Detailed description of the voucher offer |
value | number | No | >= 0. Default: 0 | Discount value. Interpretation depends on valueType |
valueType | string | No | Enum: fixed, percentage | How value is applied: fixed (flat amount) or percentage (discount rate) |
valueCurrency | string | No | ISO 4217 code (e.g., THB, HKD, USD). Default: THB | Currency for value when valueType is fixed |
minSpend | number | No | >= 0. Default: 0 | Minimum spend required before the voucher can be applied. 0 means no minimum |
maxDiscount | number | Yes | > 0; may be null when unset | Maximum discount cap for percentage vouchers |
terms | string | No | May be empty string "" | Terms and conditions for using the voucher |
images | string[] | No | Array of valid URLs. May be empty [] | Image URLs. First image is the primary display |
isActive | boolean | No | true or false | Whether the voucher is currently active and available for claiming |
isTransferable | boolean | No | true or false. Default: false | Whether a claimed voucher can be transferred between users |
totalQuantity | integer | No | -1 = unlimited; >= 0 = limited | Maximum vouchers available. No more claims when claimedQuantity >= totalQuantity |
claimedQuantity | integer | No | >= 0 | Number of vouchers already claimed by users |
maxClaimsPerUser | integer | No | 0 = unlimited; >= 1 = limited | Maximum times a single user can claim this voucher |
voucherType | string | No | Enum: cash, discount, product, cash_discount. Default: discount | Voucher business category. Do not use for tenant/external source decision |
consumptionType | string | No | Enum: vio_code, coupon_code, url, qr_code, manual, zhichong | Voucher consumption mode. Use with source fields to choose redeem input style |
externalProvider | string | Yes | Provider code or null | External source indicator. Together with externalId determines external voucher |
externalId | string | Yes | Provider-side template ID or null | External source indicator. Together with externalProvider determines external voucher |
externalRequiresDirectOrderParams | boolean | Yes | true / false / null | Whether external redeem may require account params (for example direct recharge flow) |
externalData | object | Yes | Object or null | Raw or extended external provider data. Shape may vary by provider |
category | string | Yes | Enum: Wellness, Health, Food & Beverage, Leisure & Entertainment, Travel & Hospitality, Lifestyle & Services, Others. May be null | Primary product/service category of the voucher. Use this for category-based grouping or filtering |
categories | string[] | No | Array of trimmed strings. May be [] | Legacy free-form category tags (deprecated — prefer category). Still returned for backwards compatibility |
applicableBrands | object[] | No | Each item includes _id, name, slug | Applicable brands/sub-companies. Legacy field; prefer applicableBrandTags |
applicableBrandTags | object[] | No | Each item includes _id, name, logo | Applicable brand tags |
settlementAmount | number | No | >= 0. Default: 0 | Settlement price (per voucher) that VIO accounts to the issuing tenant when the voucher is redeemed in a cross-tenant scenario. This is the wholesale/settlement value the tenant receives — useful for partner billing reconciliation |
settlementCurrency | string | No | ISO 4217 code, uppercase. Default: THB | Currency for settlementAmount |
crossTenantReceivableTiming | string | No | Enum: redemption, consumption. Default: redemption | When to record cross-tenant receivable settlement |
startDate | string | Yes | ISO 8601 datetime. null if not set | When the voucher becomes valid |
endDate | string | Yes | ISO 8601 datetime. null = no expiry | When the voucher expires |
visibility | string | No | Enum: private, public, shared | Access scope. Also controls whether the voucher can be transferred between users |
tenantId | object | No | Includes _id, name, slug | Tenant that owns the voucher |
subCompanyId | object | Yes | Includes _id, name, slug; may be null | Sub-company that owns the voucher |
isOwn | boolean | No | true or false | Whether the voucher belongs to the current request context organization |
ownerOrg | object | Yes | { "name": string, "type": "tenant/subCompany" }; usually omitted for own vouchers | Owner organization info for non-own vouchers |
targetTiers | string[] | No | Array of strings. May be [] | Membership tiers that can see or claim this voucher |
targetUserGroups | string[] | No | Array of MongoDB ObjectIds. May be [] | User groups that can see or claim this voucher |
applicableScope | string | No | Enum: all_outlets, partial_outlets, single_store. Default: all_outlets | Store applicability scope |
applicableCountry | string | Yes | ISO country/region code. May be null | Single applicable country/region. Legacy field |
applicableCountries | string[] | No | Array of ISO country/region codes. May be [] | Applicable countries/regions |
storeId | string | Yes | MongoDB ObjectId. May be null | Store ID for single-store applicability |
storeLocation | object | Yes | Includes name, address, latitude, longitude; may be null | Embedded store location |
bookingEnabled | boolean | No | true or false. Default: false | Whether booking is enabled |
bookingDaysInAdvance | integer | No | >= 0. Default: 0 | How many days in advance users can book |
consumptionMessage | string | No | May be empty string "" | Instructions for manual consumption type |
consumptionUrl | string | No | May be empty string "" | URL used for url consumption type |
consumptionQrCode | string | No | May be empty string "" | QR code image path for qr_code consumption type |
contractAddress | string | Yes | Blockchain contract address. May be null | Voucher NFT contract address |
createdBy | string | Yes | MongoDB ObjectId. May be null | Admin user ID that created/deployed the voucher |
metadata | object | No | Object. Default: {} | Extended metadata |
createdAt | string | No | ISO 8601 datetime | When the voucher was created |
updatedAt | string | No | ISO 8601 datetime | When the voucher was last modified |
Create Voucher
Create a new voucher for your tenant.
POST /vouchersRequest Body:
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
name | string | Yes | Min: 1 character. Trimmed | Voucher display name shown to users in the member app |
description | string | No | No max length. Default: "" | Detailed description of the voucher offer. Plain text only |
voucherType | string | No | Enum: cash, discount, product, cash_discount. Default: discount | Business category of the voucher |
visibility | string | No | Enum: private, public, shared. Default: private | Access scope: private (own tenant only), public (all tenants), shared (specific tenants in sharedWithTenants) |
sharedWithTenants | string[] | No | Each element: MongoDB ObjectId (24 hex chars) | Tenant IDs to share with. Only applicable when visibility is shared |
sharedWithUsers | string[] | No | Each element: MongoDB ObjectId (24 hex chars) | User IDs who can see this voucher. Used for targeted distribution |
terms | string | No | No max length. Default: "" | Terms and conditions displayed to users before claim/redeem |
value | number | No | Min: 0. Default: 0 | Discount value. When valueType is fixed, this is the flat discount amount. When percentage, this is the rate (e.g., 20 = 20% off) |
valueType | string | No | Enum: fixed, percentage. Default: fixed | How value is interpreted: fixed (flat currency amount) or percentage (discount rate) |
valueCurrency | string | No | ISO 4217 code (e.g., THB, HKD, USD). Default: THB | Currency code for value when valueType is fixed |
minSpend | number | No | Min: 0. Default: 0 | Minimum purchase amount required before voucher can be applied. 0 = no minimum |
maxDiscount | number | No | Must be > 0 (positive only) | Maximum discount cap. Useful for percentage vouchers (e.g., 20% off but max $100 discount) |
totalQuantity | integer | No | Integer only. -1 = unlimited. Default: -1 | Total vouchers available for claiming. No more claims when fully claimed |
startDate | string | No | ISO 8601 datetime (e.g., 2024-06-01T00:00:00.000Z). Default: current time | When the voucher becomes claimable. Omit for immediately available |
endDate | string | No | ISO 8601 datetime. Must be after startDate | When the voucher expires. Claims/redemptions after this are rejected. Omit for no expiry |
maxClaimsPerUser | integer | No | Integer only. Min: 0. 0 = unlimited. Default: 1 | Maximum times a single user can claim this voucher |
settlementAmount | number | No | Min: 0. Default: 0 | Fiat amount for inter-tenant settlement when redeemed in cross-tenant scenario |
settlementCurrency | string | No | ISO 4217 code. Stored uppercase. Trimmed. Default: THB | Currency for settlementAmount. Defaults to tenant's primary currency from Billing Settings. All settlement records stored in this currency |
category | string | No | Enum: Wellness, Health, Food & Beverage, Leisure & Entertainment, Travel & Hospitality, Lifestyle & Services, Others | Primary voucher category. Prefer this field for categorization, for example "Others" |
categories | string[] | No | Array of strings. Each element trimmed | Category tags for organizing vouchers (e.g., ["food", "lifestyle"]) |
images | string[] | No | Each element must be a valid URL | Image URLs. First image is used as primary display |
targetTiers | string[] | No | Array of strings. Each element trimmed | Membership tier names. Only users in these tiers can see/claim |
targetUserGroups | string[] | No | Each element: MongoDB ObjectId (24 hex chars) | User group IDs. Only users in these groups can see/claim |
subCompanyId | string | No | MongoDB ObjectId (24 hex chars) | Sub-company to scope this voucher to |
Example Request:
curl -X POST "https://your-domain.com/api/external/v1/vouchers" \
-H "X-API-Key: vio_live_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"name": "Summer Sale 20% Off",
"description": "Valid for summer collection",
"value": 20,
"valueType": "percentage",
"category": "Others",
"totalQuantity": 100,
"maxClaimsPerUser": 1,
"startDate": "2024-06-01T00:00:00.000Z",
"endDate": "2024-08-31T23:59:59.000Z",
"visibility": "public",
"terms": "Cannot be combined with other offers"
}'Example Response:
{
"success": true,
"data": {
"_id": "507f1f77bcf86cd799439012",
"name": "Summer Sale 20% Off",
"description": "Valid for summer collection",
"value": 20,
"valueType": "percentage",
"totalQuantity": 100,
"claimedQuantity": 0,
"maxClaimsPerUser": 1,
"startDate": "2024-06-01T00:00:00.000Z",
"endDate": "2024-08-31T23:59:59.000Z",
"visibility": "public",
"isActive": true,
"createdAt": "2024-05-15T10:00:00.000Z"
},
"message": "Voucher created"
}Response Fields:
Returns the created voucher object. Most fields match List Vouchers Response Fields, but list-only computed fields such as isOwn and ownerOrg are not added by this create endpoint.
Get Voucher Details
Retrieve details of a specific voucher.
GET /vouchers/:voucherIdPath Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
voucherId | string | Yes | Voucher ID |
Example Request:
curl -X GET "https://your-domain.com/api/external/v1/vouchers/507f1f77bcf86cd799439011" \
-H "X-API-Key: vio_live_your_api_key_here"Example Response:
{
"success": true,
"data": {
"_id": "507f1f77bcf86cd799439011",
"name": "20% Off Discount",
"description": "Get 20% off your next purchase",
"value": 20,
"valueType": "percentage",
"terms": "Valid on orders over $50",
"images": ["https://example.com/image.jpg"],
"isActive": true,
"totalQuantity": 100,
"claimedQuantity": 45,
"maxClaimsPerUser": 1,
"startDate": "2024-01-01T00:00:00.000Z",
"endDate": "2024-12-31T23:59:59.000Z",
"visibility": "public",
"createdAt": "2024-01-01T00:00:00.000Z",
"updatedAt": "2024-01-15T10:30:00.000Z"
}
}Response Fields:
Returns the full voucher object. Most fields match List Vouchers Response Fields, but list-only computed fields such as isOwn and ownerOrg are not added by this detail endpoint. For shared vouchers, sharedWithTenants, sharedWithSubCompanies, and sharedWithUsers may be populated with summary objects.
Update Voucher
Update an existing voucher.
PATCH /vouchers/:voucherIdPath Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
voucherId | string | Yes | Voucher ID |
Request Body:
All fields are optional. Include only the fields you want to update.
| Field | Type | Constraints | Description |
|---|---|---|---|
name | string | Min: 1 character | Voucher display name |
description | string | No max length | Voucher description |
voucherType | string | Enum: cash, discount, product, cash_discount | Business category of the voucher |
visibility | string | Enum: private, public, shared | Access scope |
sharedWithTenants | string[] | Each element: MongoDB ObjectId (24 hex chars) | Tenant IDs to share with (only for shared visibility) |
sharedWithUsers | string[] | Each element: MongoDB ObjectId (24 hex chars) | User IDs for targeted distribution |
terms | string | No max length | Terms and conditions |
value | number | Min: 0 | Discount value |
valueType | string | Enum: fixed, percentage | How value is interpreted |
valueCurrency | string | ISO 4217 code (e.g., THB, HKD, USD) | Currency for fixed-value vouchers |
minSpend | number | Min: 0 | Minimum purchase amount. 0 = no minimum |
maxDiscount | number | Must be > 0 (positive only) | Maximum discount cap for percentage vouchers |
totalQuantity | integer | Integer only. -1 = unlimited | Total available quantity |
startDate | string | ISO 8601 datetime | When voucher becomes valid |
endDate | string | ISO 8601 datetime | When voucher expires |
maxClaimsPerUser | integer | Integer only. Min: 0. 0 = unlimited | Max claims per user |
settlementAmount | number | Min: 0 | Settlement amount for cross-tenant reconciliation |
settlementCurrency | string | ISO 4217 code. Stored uppercase. Trimmed | Settlement currency. Defaults to tenant's primary currency. All records stored in tenant's currency |
category | string | Enum: Wellness, Health, Food & Beverage, Leisure & Entertainment, Travel & Hospitality, Lifestyle & Services, Others | Primary voucher category, for example "Others" |
isActive | boolean | true or false | Active status |
categories | string[] | Array of strings. Each element trimmed | Category tags |
images | string[] | Each element must be a valid URL | Image URLs |
targetTiers | string[] | Array of strings. Each element trimmed | Membership tier names for targeting |
targetUserGroups | string[] | Each element: MongoDB ObjectId (24 hex chars) | User group IDs for targeting |
Example Request:
curl -X PATCH "https://your-domain.com/api/external/v1/vouchers/507f1f77bcf86cd799439011" \
-H "X-API-Key: vio_live_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"name": "Updated Voucher Name",
"isActive": false
}'Example Response:
{
"success": true,
"data": {
"_id": "507f1f77bcf86cd799439011",
"name": "Updated Voucher Name",
"isActive": false,
"updatedAt": "2024-02-01T12:00:00.000Z"
},
"message": "Voucher updated"
}Delete Voucher
Soft delete a voucher (marks as deleted, not permanently removed).
DELETE /vouchers/:voucherIdPath Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
voucherId | string | Yes | Voucher ID |
Example Request:
curl -X DELETE "https://your-domain.com/api/external/v1/vouchers/507f1f77bcf86cd799439011" \
-H "X-API-Key: vio_live_your_api_key_here"Example Response:
{
"success": true,
"data": {
"_id": "507f1f77bcf86cd799439011",
"isDeleted": true,
"isActive": false,
"deletedAt": "2024-02-01T12:00:00.000Z"
},
"message": "Voucher deleted"
}Response Fields:
Returns the updated voucher object after soft delete. Key fields are _id, isDeleted: true, isActive: false, and deletedAt; other voucher fields may also be present.
Duplicate Voucher
Create a copy of an existing voucher.
POST /vouchers/:voucherId/duplicatePath Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
voucherId | string | Yes | Voucher ID to duplicate |
Example Request:
curl -X POST "https://your-domain.com/api/external/v1/vouchers/507f1f77bcf86cd799439011/duplicate" \
-H "X-API-Key: vio_live_your_api_key_here"Example Response:
{
"success": true,
"data": {
"_id": "507f1f77bcf86cd799439013",
"name": "20% Off Discount (Copy)",
"description": "Get 20% off your next purchase",
"value": 20,
"valueType": "percentage",
"claimedQuantity": 0,
"createdAt": "2024-02-01T12:00:00.000Z"
},
"message": "Voucher duplicated"
}Response Fields:
Returns the duplicated voucher object. The copy uses the original fields, appends (Copy) to name, resets claimedQuantity to 0, and receives a newly deployed contractAddress.
Send Voucher to User
Send (issue) a voucher directly to a user. This mints an NFT and creates an active voucher claim for the specified user, bypassing the campaign claim flow.
POST /vouchers/:voucherId/sendChoosing between /send and /send-campaign
This endpoint is the right choice when you want a direct issuance that does not consume a campaign quota and you need the full UserVoucher document (including redemptionCode, expiresAt, nftTokenId, etc.) in the response.
If you instead need the platform to automatically pick an active campaign with available quota for the voucher (typical for mini-apps that only know the voucher schema id), use Send Voucher via Campaign. The two endpoints return different response shapes — see that section for details.
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
voucherId | string | Yes | Voucher ID to send |
Request Body:
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
userId | string | Yes | Min: 1 character. MongoDB ObjectId (24 hex chars) | User ID to send the voucher to |
Example Request:
curl -X POST "https://your-domain.com/api/external/v1/vouchers/507f1f77bcf86cd799439011/send" \
-H "X-API-Key: vio_live_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"userId": "507f1f77bcf86cd799439015"
}'Example Response:
{
"success": true,
"data": {
"_id": "507f1f77bcf86cd799439050",
"userId": "507f1f77bcf86cd799439015",
"voucherId": "507f1f77bcf86cd799439011",
"tenantId": "507f1f77bcf86cd799439001",
"nftTokenId": "42",
"status": "active",
"redemptionCode": "VCH-M1ABC2-XY3Z",
"expiresAt": "2024-08-31T23:59:59.000Z",
"settlementAmount": 0,
"settlementCurrency": "HKD",
"claimedAt": "2024-02-01T12:00:00.000Z",
"createdAt": "2024-02-01T12:00:00.000Z"
},
"message": "Voucher sent to user"
}Response Fields:
| Field | Type | Nullable | Constraints / Format | Description |
|---|---|---|---|---|
_id | string | No | MongoDB ObjectId (24 hex chars) | Unique identifier for this voucher claim (UserVoucher record) |
userId | string | No | MongoDB ObjectId (24 hex chars) | The user who received the voucher |
voucherId | string | No | MongoDB ObjectId (24 hex chars) | The voucher template ID that was sent |
tenantId | string | No | MongoDB ObjectId (24 hex chars) | The tenant that owns this voucher claim |
nftTokenId | string | Yes | Numeric string (blockchain token ID). null if minting is pending | On-chain NFT token ID minted for this claim |
status | string | No | Enum: active, claimed, redeemed, expired. Always active for newly sent | Current claim status |
redemptionCode | string | No | Format: VCH-{base36_timestamp}-{4_random_chars} (e.g., VCH-M1ABC2-XY3Z). Unique | Code the user presents to redeem the voucher |
expiresAt | string | Yes | ISO 8601 datetime. null = no expiry | When this claim expires. Inherited from voucher's endDate at claim time |
settlementAmount | number | No | >= 0. Default: 0 | Settlement amount copied from voucher at claim time for cross-tenant reconciliation |
settlementCurrency | string | No | ISO 4217 code, uppercase (e.g., HKD, THB). Default: THB | Currency for settlementAmount |
claimedAt | string | No | ISO 8601 datetime | When the voucher was sent to the user |
createdAt | string | No | ISO 8601 datetime | Record creation timestamp |
Error Responses:
| Status | Code | Description |
|---|---|---|
| 400 | VALIDATION_ERROR | Voucher is not active, expired, sold out, or user at claim limit |
| 404 | NOT_FOUND | Voucher or user not found |
Send Voucher via Campaign
Send (issue) a voucher to a user through an active campaign with available quota, automatically selected by the platform from the campaigns that include this voucher schema. Most mini-apps (Lucky Draw, Receipt Claim, Stamp, Check-in, Registration Reward, Shake-for-Reward, etc.) use this endpoint because they only persist the voucher schema id in their prize / reward configuration and rely on VIO to pick a valid campaign at send time.
POST /vouchers/:voucherId/send-campaignRequired Scope: vouchers
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
voucherId | string | Yes | Voucher schema ID (Voucher._id) |
Request Body:
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
userId | string | Yes | Min: 1 character. MongoDB ObjectId (24 hex chars) | User ID to send the voucher to |
campaignId | string | No | MongoDB ObjectId (24 hex chars) | Optional explicit campaign id. If omitted, VIO auto-selects an active campaign that contains this voucher schema. |
Example Request:
curl -X POST "https://your-domain.com/api/external/v1/vouchers/507f1f77bcf86cd799439011/send-campaign" \
-H "X-API-Key: vio_live_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"userId": "507f1f77bcf86cd799439015"
}'Example Response:
{
"success": true,
"data": {
"voucherNftId": "507f1f77bcf86cd799439050",
"campaignId": "507f1f77bcf86cd799439020",
"campaignVoucherSchemaId": "507f1f77bcf86cd799439030",
"voucherSchemaId": "507f1f77bcf86cd799439011"
},
"message": "Campaign voucher sent to user"
}Response Fields:
| Field | Type | Nullable | Description |
|---|---|---|---|
voucherNftId | string | No | The created UserVoucher _id (i.e., this is the user-voucher claim id). See callout below. |
campaignId | string | No | The campaign that was selected to issue the voucher under |
campaignVoucherSchemaId | string | No | The campaign-voucher entry id (a campaign's slot for this voucher schema) |
voucherSchemaId | string | No | The voucher template (schema) id — same as the path parameter, echoed for convenience |
Response shape differs from /send
The campaign-aware endpoint does NOT return the full UserVoucher document, and the user-voucher id is exposed under voucherNftId instead of _id.
If you previously called POST /vouchers/:voucherId/send and read data._id, switching the URL to /send-campaign will silently break that read because:
/sendreturns{ success, data: { _id, voucherId, userId, redemptionCode, expiresAt, ... } }./send-campaignreturns{ success, data: { voucherNftId, campaignId, campaignVoucherSchemaId, voucherSchemaId } }.
When parsing the response, read data.voucherNftId to get the UserVoucher _id. A safe extraction that supports both shapes:
function extractUserVoucherId(body) {
const data = body?.data;
if (!data || typeof data !== 'object') return null;
return data.voucherNftId || data.userVoucherId || data._id || data.id || null;
}/send-campaign also does not return redemptionCode, expiresAt, or nftTokenId. If you need those fields, fetch them afterwards via GET /users/:userId/vouchers (filter by voucherNftId) or use /send instead.
Redirecting to the voucher detail page in the Member App
After issuing a voucher to a mini-app user, redirect them into the Member App voucher detail page using the redirectUrl provided by the host miniapp viewer:
// voucherNftId is the UserVoucher _id returned by /send-campaign
window.top.location.href = `${redirectUrl}/voucher/${voucherNftId}`;Use window.top.location.href (not window.location.href) so that navigation breaks out of the mini-app iframe.
Error Responses:
| Status | Code | Description |
|---|---|---|
| 400 | VALIDATION_ERROR | Voucher is not active / has not started / has expired / is sold out, no eligible active campaign was found, or user is at the per-user claim limit |
| 404 | NOT_FOUND | Voucher schema or user not found |
Note on
maxClaimsPerUser: If the voucher schema setsmaxClaimsPerUserand the user has already reached it, the response is HTTP 400 witherror.messagecontainingmaximum claim limit for this voucher. Mini-apps that allow re-rolling on this error (e.g. Lucky Draw) should detect this string/code and pick another prize rather than surfacing the error to the user.
List Voucher Claims
Get a list of all voucher claims for your tenant.
GET /vouchers/claims/listQuery Parameters:
| Parameter | Type | Required | Constraints | Description |
|---|---|---|---|---|
page | string | No | Parsed as integer. Min: 1. Default: 1 | Page number (1-indexed) |
limit | string | No | Parsed as integer. Min: 1. Default: 20 | Items per page |
voucherId | string | No | MongoDB ObjectId (24 hex chars) | Filter by voucher ID |
status | string | No | Enum: active, claimed, redeemed, expired | Filter by claim status |
fromDate | string | No | ISO 8601 datetime (e.g., 2024-01-01T00:00:00.000Z) | Filter claims from this date (inclusive) |
toDate | string | No | ISO 8601 datetime | Filter claims until this date (inclusive) |
search | string | No | Free-form string | Search by user name or email (partial match) |
Example Request:
curl -X GET "https://your-domain.com/api/external/v1/vouchers/claims/list?status=active&limit=10" \
-H "X-API-Key: vio_live_your_api_key_here"Example Response:
{
"success": true,
"data": [
{
"_id": "507f1f77bcf86cd799439014",
"voucherId": {
"_id": "507f1f77bcf86cd799439011",
"name": "20% Off Discount",
"images": ["https://example.com/image.jpg"],
"value": 20,
"valueType": "percentage"
},
"userId": {
"_id": "507f1f77bcf86cd799439015",
"displayName": "John Doe",
"email": "john@example.com",
"avatar": "https://example.com/avatar.jpg"
},
"tenantId": "507f1f77bcf86cd799439001",
"status": "active",
"redemptionCode": "VCH-M1ABC2-XY3Z",
"claimedAt": "2024-01-15T14:30:00.000Z",
"expiresAt": "2024-12-31T23:59:59.000Z",
"settlementAmount": 80,
"settlementCurrency": "THB"
}
],
"pagination": {
"page": 1,
"limit": 10,
"total": 45,
"totalPages": 5,
"hasNextPage": true,
"hasPrevPage": false
}
}Response Fields (each item in data array):
| Field | Type | Nullable | Constraints / Format | Description |
|---|---|---|---|---|
_id | string | No | MongoDB ObjectId (24 hex chars) | Unique claim identifier |
voucherId | object | No | Populated voucher summary | Associated voucher template |
voucherId._id | string | No | MongoDB ObjectId (24 hex chars) | Voucher template ID |
voucherId.name | string | No | Trimmed | Voucher display name |
voucherId.images | string[] | No | Array of URLs. May be [] | Voucher images |
voucherId.value | number | No | >= 0 | Voucher value |
voucherId.valueType | string | No | Enum: fixed, percentage | How value is applied |
userId | object | No | Populated user summary | User who received or claimed the voucher |
userId._id | string | No | MongoDB ObjectId (24 hex chars) | User ID |
userId.displayName | string | Yes | May be null if not set | User display name |
userId.email | string | Yes | Email format | User email |
userId.avatar | string | Yes | URL or null | User avatar |
tenantId | string | No | MongoDB ObjectId (24 hex chars) | Tenant that owns the claim record |
voucherOwnerTenantId | string | Yes | MongoDB ObjectId (24 hex chars) | Tenant that owns the voucher template |
nftTokenId | string | Yes | Blockchain token ID. May be null | NFT token minted for this claim |
status | string | No | Enum: active, claimed, redeemed, expired | Current claim status |
redemptionCode | string | No | Format: VCH-{base36_timestamp}-{4_random_chars}. Unique | Code for redeeming the voucher |
claimedAt | string | No | ISO 8601 datetime | When the voucher was claimed or sent |
redeemedAt | string | Yes | ISO 8601 datetime. May be null | When the voucher was redeemed |
expiresAt | string | Yes | ISO 8601 datetime. null = no expiry | When this claim expires |
campaignId | string | Yes | MongoDB ObjectId. May be null | Campaign associated with the claim, if any |
settlementAmount | number | No | >= 0. Default: 0 | Settlement amount captured at claim time |
settlementCurrency | string | No | ISO 4217 code | Settlement currency |
tokenAmountPaid | string | No | Numeric string. Default: "0" | Token amount paid at claim time |
redemptionDetails | object | Yes | Object or null | Redemption metadata after use |
externalVoucherCode | string | Yes | String or null | Fulfilled external voucher code |
externalRedemptionUrl | string | Yes | URL or null | Fulfilled external redemption URL |
externalOrderId | string | Yes | Provider order ID or null | External provider order ID |
externalBuyStatus | string | Yes | Enum: pending, recorded, failed, null | External buy status |
externalFulfillmentStatus | string | Yes | Enum: pending, processing, fulfilled, failed, null | External fulfillment status |
externalFulfillmentError | string | Yes | String or null | External fulfillment error details |
externalIsDirectRecharge | boolean | No | true or false. Default: false | Whether this fulfillment was direct recharge |
externalRechargeAccount | string | Yes | String or null | Direct recharge account, if applicable |
metadata | object | No | Object. Default: {} | Extended metadata |
createdAt | string | No | ISO 8601 datetime | Record creation timestamp |
updatedAt | string | No | ISO 8601 datetime | Last update timestamp |
Redeem Voucher by Code
Redeem a voucher using its redemption code. Use this when a customer presents their voucher code at your store or checkout.
POST /vouchers/redeem-by-codeRequest Body:
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
redemptionCode | string | Yes | Min: 1 character. Case-sensitive | The unique redemption code from the voucher claim (e.g., VCH-M1ABC2-XY3Z) |
location | string | No | No max length | Where the redemption occurred (e.g., store name or branch) |
notes | string | No | No max length | Additional notes about the redemption (e.g., order number) |
Example Request:
curl -X POST "https://your-domain.com/api/external/v1/vouchers/redeem-by-code" \
-H "X-API-Key: vio_live_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"redemptionCode": "ABC123XYZ",
"location": "Store #42",
"notes": "Customer purchased item XYZ"
}'Example Response:
{
"success": true,
"data": {
"_id": "507f1f77bcf86cd799439014",
"voucherId": "507f1f77bcf86cd799439011",
"userId": "507f1f77bcf86cd799439015",
"status": "redeemed",
"redemptionCode": "VCH-M1ABC2-XY3Z",
"claimedAt": "2024-01-15T14:30:00.000Z",
"redeemedAt": "2024-02-01T10:15:00.000Z",
"redemptionDetails": {
"location": "Store #42",
"notes": "Customer purchased item XYZ",
"method": "api_key",
"redeemedBy": "api:507f1f77bcf86cd799439099",
"redeemedByType": "api_key",
"redeemedByApiKey": "507f1f77bcf86cd799439099"
},
"externalVoucherCode": "ABC-123-XYZ",
"externalRedemptionUrl": null,
"externalOrderId": "69f75fe31134f32e472b2f98",
"externalFulfillmentStatus": "fulfilled"
},
"message": "Voucher redeemed"
}Response Fields:
| Field | Type | Nullable | Constraints / Format | Description |
|---|---|---|---|---|
_id | string | No | MongoDB ObjectId (24 hex chars) | Claim identifier |
voucherId | string or object | No | MongoDB ObjectId or populated voucher object | Associated voucher template |
userId | string | No | MongoDB ObjectId (24 hex chars) | User who owned the voucher |
status | string | No | redeemed after successful redemption | Claim status |
redemptionCode | string | No | Format: VCH-{base36_timestamp}-{4_random_chars} | The redeemed code |
claimedAt | string | No | ISO 8601 datetime | When the voucher was originally claimed |
redeemedAt | string | No | ISO 8601 datetime | When the voucher was redeemed |
redemptionDetails.location | string | Yes | May be omitted if not provided in request | Where the redemption occurred |
redemptionDetails.notes | string | Yes | May be omitted if not provided in request | Additional redemption notes |
redemptionDetails.method | string | No | api_key | Redemption method |
redemptionDetails.redeemedBy | string | No | api:{apiKeyId} | API key actor recorded for the redemption |
redemptionDetails.redeemedByType | string | No | api_key | Actor type |
redemptionDetails.redeemedByApiKey | string | Yes | MongoDB ObjectId | API key ID, when available |
externalVoucherCode | string | Yes | String or null | Fulfilled external voucher code |
externalRedemptionUrl | string | Yes | URL or null | Fulfilled external redemption URL |
externalOrderId | string | Yes | Provider order ID or null | External provider order ID |
externalFulfillmentStatus | string | Yes | Enum: fulfilled, failed, processing, pending, null | External fulfillment status |
Get Voucher Info by Redemption Code
Retrieve voucher information using the redemption code before redeeming. Useful for verifying voucher details before processing.
GET /vouchers/redeem/:code/infoPath Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
code | string | Yes | Redemption code |
Example Request:
curl -X GET "https://your-domain.com/api/external/v1/vouchers/redeem/ABC123XYZ/info" \
-H "X-API-Key: vio_live_your_api_key_here"Example Response:
{
"success": true,
"data": {
"redemptionCode": "VCH-M1ABC2-XY3Z",
"status": "active",
"voucher": {
"name": "20% Off Discount",
"description": "Get 20% off your next purchase",
"value": 20,
"valueType": "percentage",
"valueCurrency": "THB",
"image": "https://example.com/image.jpg",
"terms": "Valid on orders over $50"
},
"tenant": {
"name": "Demo Tenant",
"logo": "https://example.com/logo.png",
"slug": "demo-tenant"
},
"pinPrefix": "VI",
"expiresAt": "2024-12-31T23:59:59.000Z",
"canRedeem": true
}
}Response Fields:
| Field | Type | Nullable | Constraints / Format | Description |
|---|---|---|---|---|
redemptionCode | string | No | Redemption code | The code that was looked up |
status | string | No | Enum: active, claimed, redeemed, expired | Display status. Expired active claims return expired |
voucher | object | Yes | Object or null | Voucher display summary |
voucher.name | string | No | Trimmed | Voucher display name |
voucher.description | string | No | May be empty "" | Voucher description |
voucher.value | number | No | >= 0 | Discount value |
voucher.valueType | string | No | Enum: fixed, percentage | How value is applied |
voucher.valueCurrency | string | No | ISO 4217 code | Currency for fixed-value vouchers |
voucher.image | string | Yes | URL or null | First voucher image |
voucher.terms | string | No | May be empty "" | Terms and conditions |
tenant | object | Yes | Object or null | Voucher owner tenant summary |
tenant.name | string | No | Text | Tenant name |
tenant.logo | string | Yes | URL or null | Tenant logo |
tenant.slug | string | Yes | Slug or null | Tenant slug |
pinPrefix | string | No | Text, e.g. VI | PIN prefix expected for staff PIN redemption |
expiresAt | string | Yes | ISO 8601 datetime. null = no expiry | When this claim expires |
canRedeem | boolean | No | true or false | Whether the current status allows redemption |
Consume Voucher by PIN
Consume a voucher using a staff PIN. This endpoint verifies the staff member's identity via their personal PIN before marking the voucher as consumed. Use this when staff members need to authenticate themselves at the point of consumption.
Important: PIN verification is scoped to the voucher creator's organization. The PIN is validated against the tenant (or sub-company) that created the voucher, not the organization that distributed it via a campaign. In cross-tenant scenarios (e.g., Company A creates a voucher and Company B distributes it through their campaign), the member must present the voucher at Company A's store where Company A's staff enters their PIN. If the voucher was created by a sub-company, only that sub-company's PINs are valid.
PIN Permission Requirement: The staff PIN must have the
voucher_redemptionpermission to consume vouchers. PINs that only have thetoken_claimpermission cannot be used for voucher consumption. See the Admin Portal documentation for details on configuring PIN permissions.
Redeem by Code vs Consume by PIN
- Redeem by Code (
POST /vouchers/redeem-by-code): Uses the API key for authorization. The API key owner is recorded as the redeemer. Best for server-to-server integrations.- Consume by PIN (
POST /vouchers/redeem/:code/pin): Requires a staff PIN for verification. The specific staff member is recorded. Best for in-store scenarios where individual staff accountability is needed.
POST /vouchers/redeem/:code/pinPath Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
code | string | Yes | The redemption code from the voucher claim |
Request Body:
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
pin | string | Yes | Min: 5 characters. Max: 20 characters. Typical format: 2-letter prefix + 4-digit PIN (e.g., HA1234). Must have voucher_redemption permission | Staff PIN for identity verification at point of consumption |
Example Request:
curl -X POST "https://your-domain.com/api/external/v1/vouchers/redeem/ABC123XYZ/pin" \
-H "X-API-Key: vio_live_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"pin": "HA1234"
}'Example Response:
{
"success": true,
"data": {
"success": true,
"voucher": {
"name": "20% Off Discount",
"value": 20,
"valueType": "percentage",
"valueCurrency": "THB"
},
"redeemedAt": "2024-02-01T10:15:00.000Z",
"redeemedBy": "John Staff"
},
"message": "Voucher consumed"
}Response Fields:
| Field | Type | Nullable | Constraints / Format | Description |
|---|---|---|---|---|
success | boolean | No | Always true on success | Consumption result |
voucher.name | string | No | Trimmed | Name of the consumed voucher |
voucher.value | number | No | >= 0 | Discount value |
voucher.valueType | string | No | Enum: fixed, percentage | How value is applied |
voucher.valueCurrency | string | No | ISO 4217 code (e.g., THB, HKD) | Currency for fixed-value vouchers |
redeemedAt | string | No | ISO 8601 datetime | When the voucher was consumed |
redeemedBy | string | No | Staff member's display name | Name of the staff who verified the PIN |
Error Responses:
| Status | Code | Description |
|---|---|---|
| 400 | VALIDATION_ERROR | Invalid PIN format (must be 5-20 characters) |
| 400 | VALIDATION_ERROR | Invalid PIN (wrong prefix or PIN doesn't match any staff) |
| 400 | VALIDATION_ERROR | PIN lacks voucher_redemption permission |
| 400 | VALIDATION_ERROR | Voucher is already redeemed or expired |
| 404 | NOT_FOUND | Voucher not found with the given redemption code |
Redeem Voucher with Tokens (Atomic Exchange)
Atomically deduct tokens from a user's balance and issue a voucher, in a single API call. This endpoint performs the same operation as a member self-claiming a voucher from a campaign — including token deduction, company wallet credit, NFT minting, stock reservation, and settlement recording — but triggered server-to-server via API key.
Required Scopes: vouchers AND tokens (the API key must have both scopes)
POST /vouchers/:voucherId/redeem-with-tokensPath Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
voucherId | string | Yes | Voucher ID to claim for the user |
Request Body:
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
userId | string | Yes | Min: 1 character. MongoDB ObjectId | The user who will receive the voucher and pay tokens |
campaignId | string | Yes | Min: 1 character. MongoDB ObjectId | Campaign that determines the token price and quota |
idempotencyKey | string | No | Max: 128 characters | Prevents duplicate claims. If the same key is resubmitted, the original result is returned |
Example Request:
curl -X POST "https://your-domain.com/api/external/v1/vouchers/507f1f77bcf86cd799439011/redeem-with-tokens" \
-H "X-API-Key: vio_live_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"userId": "507f1f77bcf86cd799439015",
"campaignId": "507f1f77bcf86cd799439020",
"idempotencyKey": "order-12345-voucher-abc"
}'Example Response (first call):
{
"success": true,
"data": {
"userVoucher": {
"_id": "507f1f77bcf86cd799439050",
"userId": "507f1f77bcf86cd799439015",
"voucherId": "507f1f77bcf86cd799439011",
"tenantId": "507f1f77bcf86cd799439001",
"campaignId": "507f1f77bcf86cd799439020",
"nftTokenId": "42",
"status": "active",
"redemptionCode": "VCH-M1ABC2-XY3Z",
"expiresAt": "2026-12-31T23:59:59.000Z",
"tokenAmountPaid": "100",
"settlementAmount": 0,
"settlementCurrency": "THB",
"claimedAt": "2026-06-18T06:00:00.000Z"
},
"voucher": {
"_id": "507f1f77bcf86cd799439011",
"name": "20% Off Discount"
},
"nftTokenId": "42",
"growthPointsEarned": 10
},
"message": "Voucher claimed with tokens"
}Example Response (idempotent replay):
{
"success": true,
"data": {
"userVoucher": { "..." },
"idempotent": true
},
"message": "Voucher already claimed (idempotent)"
}Response Fields:
| Field | Type | Nullable | Description |
|---|---|---|---|
userVoucher | object | No | The created UserVoucher record (same shape as /send response) |
userVoucher.tokenAmountPaid | string | No | Actual tokens deducted (after tier discount, if any) |
userVoucher.campaignId | string | No | The campaign this claim was made from |
voucher | object | No | The voucher template summary |
nftTokenId | string | Yes | On-chain NFT token ID minted for this claim |
growthPointsEarned | number | Yes | Growth points awarded for this paid redemption (only if cost > 0) |
idempotent | boolean | Yes | true when the response is a replay of a previous successful call |
Atomicity Guarantees:
This endpoint provides the following guarantees:
- Token deduction is atomic: Uses conditional MongoDB updates (
balance >= cost) to prevent double-spending under concurrent requests. - Compensating rollback: If any step fails after token deduction (NFT minting, record creation, etc.), all changes are reversed — tokens are restored to the user, stock counters are released, and partial records are cleaned up.
- Idempotency: When
idempotencyKeyis provided, a duplicate request returns the original result without performing any additional deductions.
What happens internally:
- Validates campaign, voucher, user eligibility, and tier restrictions
- Checks and reserves voucher stock + campaign quota + per-user claim limit
- Deducts tokens from user's balance (FIFO lot consumption)
- Credits tokens to the company admin wallet
- Creates token transaction record
- Mints NFT to user's blockchain wallet
- Creates UserVoucher record
- Records settlement transaction (
processedByType: api_key) - Awards growth points (if applicable)
Error Responses:
| Status | Code | Description |
|---|---|---|
| 400 | VALIDATION_ERROR | Campaign not active, ended, or not started |
| 400 | VALIDATION_ERROR | Voucher not available in this campaign |
| 400 | VALIDATION_ERROR | Insufficient token balance |
| 400 | VALIDATION_ERROR | Voucher sold out or campaign quota exhausted |
| 400 | VALIDATION_ERROR | User has reached maximum claim limit |
| 400 | VALIDATION_ERROR | User's membership tier not eligible for this campaign |
| 403 | FORBIDDEN | API key missing required tokens scope |
| 404 | NOT_FOUND | Voucher, user, or campaign not found |
7. API Reference: Users
Required Scope: users
List Users
Retrieve a paginated list of users for your tenant.
GET /usersQuery Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
page | integer | No | Page number (default: 1) |
limit | integer | No | Items per page (default: 20) |
role | string | No | Filter by role: super_admin, tenant_admin, sub_company_admin, member |
search | string | No | Search by email, phone, or name |
isActive | boolean | No | Filter by active status |
subCompanyId | string | No | Filter by sub-company |
Example Request:
curl -X GET "https://your-domain.com/api/external/v1/users?role=member&isActive=true&limit=10" \
-H "X-API-Key: vio_live_your_api_key_here"Example Response:
{
"success": true,
"data": [
{
"_id": "507f1f77bcf86cd799439015",
"email": "john@example.com",
"phone": "+66812345678",
"displayName": "John Doe",
"role": "member",
"isActive": true,
"walletAddress": "0x1234567890abcdef...",
"registrationSource": "direct",
"storeId": null,
"campaignId": null,
"createdAt": "2024-01-01T00:00:00.000Z",
"updatedAt": "2024-01-15T10:30:00.000Z"
}
],
"pagination": {
"page": 1,
"limit": 10,
"total": 150,
"totalPages": 15,
"hasNextPage": true,
"hasPrevPage": false
}
}Create User
Create a new user for your tenant. Deduplication is scoped to the current portal (tenant + sub-company) — the same email/phone can exist in different portals. A GlobalIdentity is automatically created to enable cross-tenant SSO.
POST /usersRequest Body:
| Field | Type | Required | Description |
|---|---|---|---|
email | string | No* | User email address |
phone | string | No* | User phone number |
password | string | Yes | Password (min 6 characters) |
displayName | string | No | User display name |
role | string | No | member or sub_company_admin (default: member) |
subCompanyId | string | No | Sub-company ID to assign user to |
metadata | object | No | Custom metadata key-value pairs |
*At least one of
phoneis required.
Example Request:
curl -X POST "https://your-domain.com/api/external/v1/users" \
-H "X-API-Key: vio_live_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"email": "newuser@example.com",
"phone": "+66812345678",
"password": "securepassword123",
"displayName": "New User",
"role": "member",
"metadata": {
"referralSource": "website",
"tier": "gold"
}
}'Example Response:
{
"success": true,
"data": {
"_id": "507f1f77bcf86cd799439016",
"email": "newuser@example.com",
"phone": "+66812345678",
"displayName": "New User",
"role": "member",
"isActive": true,
"walletAddress": "0xabcdef1234567890...",
"metadata": {
"referralSource": "website",
"tier": "gold"
},
"createdAt": "2024-02-01T12:00:00.000Z"
},
"message": "User created"
}Find or Create User
Idempotent endpoint that returns an existing user if found in your portal, or creates one automatically. If the identifier (email/phone) already exists in another tenant, a linked profile is created using the existing global identity — the user's display name, avatar, and wallet are shared across tenants.
POST /users/find-or-createRequest Body:
| Field | Type | Required | Description |
|---|---|---|---|
email | string | No* | User email address |
phone | string | No* | User phone number |
password | string | Yes | Password (min 6 characters) |
displayName | string | No | Display name (used only for new users) |
subCompanyId | string | No | Sub-company ID to assign user to |
metadata | object | No | Custom metadata key-value pairs |
*At least one of
phoneis required.
Example Request:
curl -X POST "https://your-domain.com/api/external/v1/users/find-or-create" \
-H "X-API-Key: vio_live_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"email": "alice@example.com",
"password": "securepassword123",
"displayName": "Alice"
}'Response Scenarios:
The response always includes two boolean flags:
| Flag | Description |
|---|---|
created | true if a new user was created, false if an existing one was returned |
linked | true if the user was linked to an existing cross-tenant identity |
Scenario 1 — User already exists in this portal (HTTP 200):
{
"success": true,
"data": {
"_id": "507f1f77bcf86cd799439016",
"email": "alice@example.com",
"displayName": "Alice",
"isActive": true,
"created": false,
"linked": false
},
"message": "User already exists in this portal"
}Scenario 2 — User exists in another tenant, linked profile created (HTTP 201):
{
"success": true,
"data": {
"_id": "507f1f77bcf86cd799439017",
"email": "alice@example.com",
"displayName": "Alice",
"globalIdentityId": "609f1f77bcf86cd799439099",
"walletAddress": "0xabcdef...",
"isActive": true,
"created": true,
"linked": true
},
"message": "User profile created and linked to existing identity"
}Scenario 3 — Brand new user (HTTP 201):
{
"success": true,
"data": {
"_id": "507f1f77bcf86cd799439018",
"email": "alice@example.com",
"displayName": "Alice",
"globalIdentityId": "609f1f77bcf86cd799439100",
"walletAddress": "0x123456...",
"isActive": true,
"created": true,
"linked": false
},
"message": "User created"
}When to use find-or-create vs. create
Use POST /users/find-or-create when syncing users from an external system — it is safe to call repeatedly and handles cross-tenant linking automatically. Use POST /users when you need strict control and want an error if the user already exists.
Get User Details
Retrieve details of a specific user.
GET /users/:userIdPath Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
userId | string | Yes | User ID |
Example Request:
curl -X GET "https://your-domain.com/api/external/v1/users/507f1f77bcf86cd799439015" \
-H "X-API-Key: vio_live_your_api_key_here"Example Response:
{
"success": true,
"data": {
"_id": "507f1f77bcf86cd799439015",
"email": "john@example.com",
"phone": "+66812345678",
"displayName": "John Doe",
"role": "member",
"isActive": true,
"walletAddress": "0x1234567890abcdef...",
"avatar": "https://example.com/avatar.jpg",
"metadata": {
"tier": "gold"
},
"createdAt": "2024-01-01T00:00:00.000Z",
"updatedAt": "2024-01-15T10:30:00.000Z"
}
}Update User
Update an existing user.
PATCH /users/:userIdPath Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
userId | string | Yes | User ID |
Request Body:
| Field | Type | Description |
|---|---|---|
displayName | string | User display name |
avatar | string | Avatar URL |
isActive | boolean | Active status |
subCompanyId | string | Sub-company ID |
metadata | object | Custom metadata |
Example Request:
curl -X PATCH "https://your-domain.com/api/external/v1/users/507f1f77bcf86cd799439015" \
-H "X-API-Key: vio_live_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"displayName": "John Smith",
"metadata": {
"tier": "platinum"
}
}'Example Response:
{
"success": true,
"data": {
"_id": "507f1f77bcf86cd799439015",
"displayName": "John Smith",
"metadata": {
"tier": "platinum"
},
"updatedAt": "2024-02-01T12:00:00.000Z"
},
"message": "User updated"
}Deactivate User
Soft delete a user (sets isActive to false).
DELETE /users/:userIdPath Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
userId | string | Yes | User ID |
Example Request:
curl -X DELETE "https://your-domain.com/api/external/v1/users/507f1f77bcf86cd799439015" \
-H "X-API-Key: vio_live_your_api_key_here"Example Response:
{
"success": true,
"data": null,
"message": "User deactivated"
}Get User Token Balances
Retrieve all token balances for a specific user.
GET /users/:userId/balancesPath Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
userId | string | Yes | User ID |
Example Request:
curl -X GET "https://your-domain.com/api/external/v1/users/507f1f77bcf86cd799439015/balances" \
-H "X-API-Key: vio_live_your_api_key_here"Example Response:
{
"success": true,
"data": [
{
"tokenId": "507f1f77bcf86cd799439020",
"tokenName": "Loyalty Points",
"symbol": "LP",
"balance": "1500",
"lockedBalance": "0"
},
{
"tokenId": "507f1f77bcf86cd799439021",
"tokenName": "Reward Coins",
"symbol": "RC",
"balance": "250",
"lockedBalance": "50"
}
]
}Get User Vouchers
Retrieve all vouchers claimed by a specific user.
GET /users/:userId/vouchersPath Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
userId | string | Yes | User ID |
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
page | integer | No | Page number (default: 1) |
limit | integer | No | Items per page (default: 20) |
status | string | No | Filter by status: active, redeemed, expired |
Example Request:
curl -X GET "https://your-domain.com/api/external/v1/users/507f1f77bcf86cd799439015/vouchers?status=active" \
-H "X-API-Key: vio_live_your_api_key_here"Example Response:
{
"success": true,
"data": [
{
"_id": "507f1f77bcf86cd799439014",
"voucherId": "507f1f77bcf86cd799439011",
"status": "active",
"redemptionCode": "ABC123XYZ",
"claimedAt": "2024-01-15T14:30:00.000Z",
"expiresAt": "2024-12-31T23:59:59.000Z",
"voucher": {
"name": "20% Off Discount",
"value": 20,
"valueType": "percentage"
}
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 5,
"totalPages": 1,
"hasNextPage": false,
"hasPrevPage": false
}
}Search User by Identifier
Find a user by email, phone, or wallet address.
GET /users/search/by-identifierQuery Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
email | string | No* | User email address |
phone | string | No* | User phone number |
walletAddress | string | No* | User wallet address |
*At least one identifier is required.
Example Request:
curl -X GET "https://your-domain.com/api/external/v1/users/search/by-identifier?email=john@example.com" \
-H "X-API-Key: vio_live_your_api_key_here"Example Response:
{
"success": true,
"data": {
"_id": "507f1f77bcf86cd799439015",
"email": "john@example.com",
"phone": "+66812345678",
"displayName": "John Doe",
"role": "member",
"isActive": true,
"walletAddress": "0x1234567890abcdef..."
}
}8. API Reference: Campaigns
Required Scope: campaigns
List Campaigns
Retrieve a paginated list of campaigns.
GET /campaignsQuery Parameters:
| Parameter | Type | Required | Constraints | Description |
|---|---|---|---|---|
page | string | No | Parsed as integer. Min: 1. Default: 1 | Page number (1-indexed) |
limit | string | No | Parsed as integer. Min: 1. Default: 20 | Items per page |
search | string | No | Free-form string | Search by campaign name (partial match) |
isActive | string | No | String "true" or "false" (parsed as boolean) | Filter by active status |
Example Request:
curl -X GET "https://your-domain.com/api/external/v1/campaigns?isActive=true&limit=10" \
-H "X-API-Key: vio_live_your_api_key_here"Example Response:
{
"success": true,
"data": [
{
"_id": "507f1f77bcf86cd799439030",
"name": "Summer Promotion",
"description": "Summer 2024 voucher campaign",
"slug": "summer-promotion",
"tokenId": "507f1f77bcf86cd799439020",
"isActive": true,
"isPublic": true,
"startDate": "2024-06-01T00:00:00.000Z",
"endDate": "2024-08-31T23:59:59.000Z",
"images": ["https://example.com/campaign.jpg"],
"createdAt": "2024-05-15T10:00:00.000Z"
}
],
"pagination": {
"page": 1,
"limit": 10,
"total": 5,
"totalPages": 1,
"hasNextPage": false,
"hasPrevPage": false
}
}Response Fields (each item in data array):
| Field | Type | Nullable | Constraints / Format | Description |
|---|---|---|---|---|
_id | string | No | MongoDB ObjectId (24 hex chars) | Unique identifier for the campaign |
name | string | No | Min: 1 char. Trimmed | Campaign display name |
description | string | No | May be empty "" | Campaign description text |
slug | string | No | Lowercase, URL-safe. Unique per tenant + sub-company | URL-friendly identifier for campaign pages (e.g., summer-promotion) |
tokenId | string | No | MongoDB ObjectId (24 hex chars) | Token users spend to claim vouchers in this campaign |
isActive | boolean | No | true or false | Whether the campaign is currently active |
isPublic | boolean | No | true or false | Whether the campaign is publicly accessible without authentication |
startDate | string | Yes | ISO 8601 datetime. Default: creation time | When the campaign starts |
endDate | string | Yes | ISO 8601 datetime. null = no end date | When the campaign ends |
images | string[] | No | Array of valid URLs. May be empty [] | Campaign banner/cover images |
createdAt | string | No | ISO 8601 datetime | Creation timestamp |
Create Campaign
Create a new voucher campaign.
POST /campaignsRequest Body:
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
name | string | Yes | Min: 1 character. Trimmed | Campaign display name. Used to auto-generate slug if not provided |
description | string | No | No max length. Default: "" | Campaign description text |
slug | string | No | Lowercase, URL-safe characters only. Unique per tenant + sub-company. Auto-generated from name via slugify if omitted | URL-friendly identifier used in campaign page URLs (e.g., summer-promotion-2024) |
tokenId | string | Yes | Min: 1 character. MongoDB ObjectId (24 hex chars). Must reference an existing Token | Token that users spend to claim vouchers in this campaign |
startDate | string | No | ISO 8601 datetime (e.g., 2024-06-01T00:00:00.000Z). Default: current time | When the campaign starts accepting claims |
endDate | string | No | ISO 8601 datetime. Should be after startDate | When the campaign ends. Omit for no end date |
isActive | boolean | No | Default: true | Whether the campaign is active. Inactive campaigns are hidden from users |
isPublic | boolean | No | Default: true | Whether the campaign appears on public pages (accessible without login) |
images | string[] | No | Each element must be a valid URL. May be empty [] | Campaign banner/cover image URLs. First image is used as primary display |
Example Request:
curl -X POST "https://your-domain.com/api/external/v1/campaigns" \
-H "X-API-Key: vio_live_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"name": "Summer Promotion 2024",
"description": "Exclusive summer deals for loyal customers",
"tokenId": "507f1f77bcf86cd799439020",
"startDate": "2024-06-01T00:00:00.000Z",
"endDate": "2024-08-31T23:59:59.000Z",
"isPublic": true
}'Example Response:
{
"success": true,
"data": {
"_id": "507f1f77bcf86cd799439031",
"name": "Summer Promotion 2024",
"description": "Exclusive summer deals for loyal customers",
"slug": "summer-promotion-2024",
"tokenId": "507f1f77bcf86cd799439020",
"isActive": true,
"isPublic": true,
"startDate": "2024-06-01T00:00:00.000Z",
"endDate": "2024-08-31T23:59:59.000Z",
"createdAt": "2024-05-15T10:00:00.000Z"
},
"message": "Campaign created"
}Response Fields:
Returns the newly created campaign object. See List Campaigns Response Fields for field descriptions.
Get Campaign Details
Retrieve details of a specific campaign.
GET /campaigns/:campaignIdPath Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
campaignId | string | Yes | Campaign ID |
Example Request:
curl -X GET "https://your-domain.com/api/external/v1/campaigns/507f1f77bcf86cd799439030" \
-H "X-API-Key: vio_live_your_api_key_here"Example Response:
{
"success": true,
"data": {
"_id": "507f1f77bcf86cd799439030",
"name": "Summer Promotion",
"description": "Summer 2024 voucher campaign",
"slug": "summer-promotion",
"tokenId": "507f1f77bcf86cd799439020",
"isActive": true,
"isPublic": true,
"startDate": "2024-06-01T00:00:00.000Z",
"endDate": "2024-08-31T23:59:59.000Z",
"images": ["https://example.com/campaign.jpg"],
"createdAt": "2024-05-15T10:00:00.000Z"
}
}Response Fields:
Returns the full campaign object. See List Campaigns Response Fields for field descriptions.
Update Campaign
Update an existing campaign.
PATCH /campaigns/:campaignIdPath Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
campaignId | string | Yes | Campaign ID |
Request Body:
| Field | Type | Constraints | Description |
|---|---|---|---|
name | string | Min: 1 character. Trimmed | Campaign display name |
description | string | No max length | Campaign description |
tokenId | string | MongoDB ObjectId (24 hex chars). Must reference existing Token | Token for the campaign |
startDate | string | ISO 8601 datetime | Campaign start date |
endDate | string | ISO 8601 datetime | Campaign end date |
isActive | boolean | true or false | Active status |
isPublic | boolean | true or false | Public visibility |
images | string[] | Each element must be a valid URL | Campaign image URLs |
Example Request:
curl -X PATCH "https://your-domain.com/api/external/v1/campaigns/507f1f77bcf86cd799439030" \
-H "X-API-Key: vio_live_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"name": "Extended Summer Promotion",
"endDate": "2024-09-30T23:59:59.000Z"
}'Example Response:
{
"success": true,
"data": {
"_id": "507f1f77bcf86cd799439030",
"name": "Extended Summer Promotion",
"endDate": "2024-09-30T23:59:59.000Z",
"updatedAt": "2024-08-15T10:00:00.000Z"
},
"message": "Campaign updated"
}Delete Campaign
Delete a campaign.
DELETE /campaigns/:campaignIdPath Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
campaignId | string | Yes | Campaign ID |
Example Request:
curl -X DELETE "https://your-domain.com/api/external/v1/campaigns/507f1f77bcf86cd799439030" \
-H "X-API-Key: vio_live_your_api_key_here"Example Response:
{
"success": true,
"data": {
"_id": "507f1f77bcf86cd799439030"
},
"message": "Campaign deleted"
}Response Fields:
| Field | Type | Constraints / Format | Description |
|---|---|---|---|
_id | string | MongoDB ObjectId (24 hex chars) | ID of the deleted campaign |
Get Campaign Vouchers
Retrieve vouchers associated with a campaign.
GET /campaigns/:campaignId/vouchersPath Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
campaignId | string | Yes | Campaign ID |
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
page | integer | No | Page number (default: 1) |
limit | integer | No | Items per page (default: 20) |
Example Request:
curl -X GET "https://your-domain.com/api/external/v1/campaigns/507f1f77bcf86cd799439030/vouchers" \
-H "X-API-Key: vio_live_your_api_key_here"Example Response:
{
"success": true,
"data": [
{
"_id": "507f1f77bcf86cd799439011",
"name": "20% Off Discount",
"value": 20,
"valueType": "percentage",
"tokenAmount": "100",
"sortOrder": 1,
"isActive": true
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 3,
"totalPages": 1,
"hasNextPage": false,
"hasPrevPage": false
}
}Response Fields (each item in data array):
| Field | Type | Nullable | Constraints / Format | Description |
|---|---|---|---|---|
_id | string | No | MongoDB ObjectId (24 hex chars) | Voucher template ID |
name | string | No | Trimmed | Voucher display name |
value | number | No | >= 0 | Discount value (see valueType) |
valueType | string | No | Enum: fixed, percentage | How value is applied |
tokenAmount | string | No | Stored as string for precision. "0" = no campaign-level cap | Campaign stock/quantity cap for this voucher |
sortOrder | integer | No | Default: 0. Lower = first | Display order within the campaign |
isActive | boolean | No | true or false | Whether this voucher is currently claimable within the campaign |
Add Vouchers to Campaign
Add vouchers to a campaign with token pricing.
POST /campaigns/:campaignId/vouchersPath Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
campaignId | string | Yes | Campaign ID |
Request Body:
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
voucherIds | string[] | Yes | Min: 1 element. Each: MongoDB ObjectId (24 hex chars). Duplicates in same campaign are rejected | Array of voucher IDs to add to the campaign |
tokenAmount | string | Yes | Min: 1 character. Stored as string for precision. "0" = no campaign-level cap | Campaign stock/quantity cap for these vouchers |
Example Request:
curl -X POST "https://your-domain.com/api/external/v1/campaigns/507f1f77bcf86cd799439030/vouchers" \
-H "X-API-Key: vio_live_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"voucherIds": ["507f1f77bcf86cd799439011", "507f1f77bcf86cd799439012"],
"tokenAmount": "100"
}'Example Response:
{
"success": true,
"data": {
"added": 2
},
"message": "Vouchers added to campaign"
}Response Fields:
| Field | Type | Constraints / Format | Description |
|---|---|---|---|
added | integer | >= 0 | Number of vouchers successfully added to campaign |
Update Campaign Voucher
Update a voucher's settings within a campaign.
PATCH /campaigns/:campaignId/vouchers/:voucherIdPath Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
campaignId | string | Yes | Campaign ID |
voucherId | string | Yes | Voucher ID |
Request Body:
| Field | Type | Constraints | Description |
|---|---|---|---|
tokenAmount | string | Stored as string for precision. "0" = no campaign-level cap | Campaign stock/quantity cap for this voucher |
sortOrder | integer | Number type (can be negative). Default: 0. Lower = first | Display order within the campaign |
isActive | boolean | true or false | Whether this voucher is claimable in the campaign. Can be temporarily disabled |
Example Request:
curl -X PATCH "https://your-domain.com/api/external/v1/campaigns/507f1f77bcf86cd799439030/vouchers/507f1f77bcf86cd799439011" \
-H "X-API-Key: vio_live_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"tokenAmount": "150",
"sortOrder": 1
}'Example Response:
{
"success": true,
"data": {
"campaignId": "507f1f77bcf86cd799439030",
"voucherId": "507f1f77bcf86cd799439011",
"tokenAmount": "150",
"sortOrder": 1
},
"message": "Campaign voucher updated"
}Response Fields:
| Field | Type | Constraints / Format | Description |
|---|---|---|---|
campaignId | string | MongoDB ObjectId (24 hex chars) | Campaign ID |
voucherId | string | MongoDB ObjectId (24 hex chars) | Voucher ID |
tokenAmount | string | String for precision. "0" = no cap | Updated campaign quantity cap |
sortOrder | integer | Lower = first | Updated display order |
Remove Voucher from Campaign
Remove a voucher from a campaign.
DELETE /campaigns/:campaignId/vouchers/:voucherIdPath Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
campaignId | string | Yes | Campaign ID |
voucherId | string | Yes | Voucher ID |
Example Request:
curl -X DELETE "https://your-domain.com/api/external/v1/campaigns/507f1f77bcf86cd799439030/vouchers/507f1f77bcf86cd799439011" \
-H "X-API-Key: vio_live_your_api_key_here"Example Response:
{
"success": true,
"data": null,
"message": "Voucher removed from campaign"
}Get Available Vouchers for Campaign
Get vouchers that can be added to a campaign (not already in the campaign).
GET /campaigns/:campaignId/available-vouchersPath Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
campaignId | string | Yes | Campaign ID |
Query Parameters:
| Parameter | Type | Required | Constraints | Description |
|---|---|---|---|---|
page | string | No | Parsed as integer. Min: 1. Default: 1 | Page number (1-indexed) |
limit | string | No | Parsed as integer. Min: 1. Default: 20 | Items per page |
search | string | No | Free-form string | Search by voucher name (partial match) |
Example Request:
curl -X GET "https://your-domain.com/api/external/v1/campaigns/507f1f77bcf86cd799439030/available-vouchers?search=discount" \
-H "X-API-Key: vio_live_your_api_key_here"Example Response:
{
"success": true,
"data": [
{
"_id": "507f1f77bcf86cd799439013",
"name": "10% New User Discount",
"value": 10,
"valueType": "percentage",
"isActive": true
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 10,
"totalPages": 1,
"hasNextPage": false,
"hasPrevPage": false
}
}Response Fields (each item in data array):
| Field | Type | Nullable | Constraints / Format | Description |
|---|---|---|---|---|
_id | string | No | MongoDB ObjectId (24 hex chars) | Voucher template ID |
name | string | No | Trimmed | Voucher display name |
value | number | No | >= 0 | Discount value |
valueType | string | No | Enum: fixed, percentage | How value is applied |
isActive | boolean | No | true or false | Whether the voucher is active |
9. API Reference: Tokens
Required Scope: tokens
List Tokens
Retrieve a list of tokens for your tenant.
GET /tokensQuery Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
page | integer | No | Page number (default: 1) |
limit | integer | No | Items per page (default: 20) |
isActive | boolean | No | Filter by active status |
subCompanyId | string | No | Filter by sub-company |
Example Request:
curl -X GET "https://your-domain.com/api/external/v1/tokens?isActive=true" \
-H "X-API-Key: vio_live_your_api_key_here"Example Response:
{
"success": true,
"data": [
{
"_id": "507f1f77bcf86cd799439020",
"name": "Loyalty Points",
"symbol": "LP",
"description": "Earn points on every purchase",
"totalSupply": "1000000",
"isActive": true,
"createdAt": "2024-01-01T00:00:00.000Z"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 2,
"totalPages": 1,
"hasNextPage": false,
"hasPrevPage": false
}
}Get Token Details
Retrieve details of a specific token.
GET /tokens/:tokenIdPath Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
tokenId | string | Yes | Token ID |
Example Request:
curl -X GET "https://your-domain.com/api/external/v1/tokens/507f1f77bcf86cd799439020" \
-H "X-API-Key: vio_live_your_api_key_here"Example Response:
{
"success": true,
"data": {
"_id": "507f1f77bcf86cd799439020",
"name": "Loyalty Points",
"symbol": "LP",
"description": "Earn points on every purchase",
"totalSupply": "1000000",
"isActive": true,
"decimals": 0,
"createdAt": "2024-01-01T00:00:00.000Z",
"updatedAt": "2024-01-15T10:30:00.000Z"
}
}Get Token Statistics
Retrieve statistics for a specific token.
GET /tokens/:tokenId/statsPath Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
tokenId | string | Yes | Token ID |
Example Request:
curl -X GET "https://your-domain.com/api/external/v1/tokens/507f1f77bcf86cd799439020/stats" \
-H "X-API-Key: vio_live_your_api_key_here"Example Response:
{
"success": true,
"data": {
"totalSupply": "1000000",
"circulatingSupply": "750000",
"holdersCount": 1250,
"totalTransactions": 15000,
"averageBalance": "600"
}
}Get Token Holders
Retrieve a list of token holders with their balances.
GET /tokens/:tokenId/holdersPath Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
tokenId | string | Yes | Token ID |
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
page | integer | No | Page number (default: 1) |
limit | integer | No | Items per page (default: 20) |
sort | string | No | Sort by: balance, name, recent (default: balance) |
search | string | No | Search by user name or email |
Example Request:
curl -X GET "https://your-domain.com/api/external/v1/tokens/507f1f77bcf86cd799439020/holders?sort=balance&limit=10" \
-H "X-API-Key: vio_live_your_api_key_here"Example Response:
{
"success": true,
"data": [
{
"user": {
"_id": "507f1f77bcf86cd799439015",
"displayName": "John Doe",
"email": "john@example.com"
},
"balance": "5000"
},
{
"user": {
"_id": "507f1f77bcf86cd799439016",
"displayName": "Jane Smith",
"email": "jane@example.com"
},
"balance": "3500"
}
],
"pagination": {
"page": 1,
"limit": 10,
"total": 1250,
"totalPages": 125,
"hasNextPage": true,
"hasPrevPage": false
},
"token": {
"_id": "507f1f77bcf86cd799439020",
"name": "Loyalty Points",
"symbol": "LP"
}
}Mint Tokens
Create new tokens and add them to a user's balance.
POST /tokens/:tokenId/mintPath Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
tokenId | string | Yes | Token ID |
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
toUserId | string | Yes | User ID to receive tokens |
amount | string | Yes | Amount to mint |
memo | string | No | Transaction memo |
Example Request:
curl -X POST "https://your-domain.com/api/external/v1/tokens/507f1f77bcf86cd799439020/mint" \
-H "X-API-Key: vio_live_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"toUserId": "507f1f77bcf86cd799439015",
"amount": "1000",
"memo": "Welcome bonus"
}'Example Response:
{
"success": true,
"data": {
"_id": "507f1f77bcf86cd799439040",
"tokenId": "507f1f77bcf86cd799439020",
"type": "mint",
"amount": "1000",
"toUserId": "507f1f77bcf86cd799439015",
"memo": "Welcome bonus",
"createdAt": "2024-02-01T12:00:00.000Z"
},
"message": "Tokens minted successfully"
}Mint Tokens to Company
Mint tokens directly to another company's admin account (identified by email or organization URL). Only the token creator (the tenant/sub-company bound to your API key) can use this endpoint.
- PUBLIC tokens: can be minted to any company.
- SHARED tokens: the target company must be in the token's sharing list.
- PRIVATE tokens: cannot be minted to other companies.
POST /tokens/:tokenId/mint-to-companyPath Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
tokenId | string | Yes | Token ID |
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
identifier | string | Yes | Target company admin email or organization URL/slug |
amount | string | Yes | Amount to mint |
memo | string | No | Transaction memo |
expiryType | string | No | permanent (default), fixed_date, or duration_days |
expiresAt | string | No | ISO date string. Required when expiryType is fixed_date |
expiryDays | number | No | Positive integer. Required when expiryType is duration_days |
Example Request:
curl -X POST "https://your-domain.com/api/external/v1/tokens/507f1f77bcf86cd799439020/mint-to-company" \
-H "X-API-Key: vio_live_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"identifier": "admin@partner-company.com",
"amount": "10000",
"memo": "Partner allocation"
}'Example Response:
{
"success": true,
"data": {
"_id": "507f1f77bcf86cd799439043",
"tokenId": "507f1f77bcf86cd799439020",
"type": "mint",
"amount": "10000",
"recipientAdmin": {
"id": "507f1f77bcf86cd799439099",
"email": "admin@partner-company.com",
"displayName": "Partner Admin"
},
"recipientOrg": {
"tenant": { "id": "507f1f77bcf86cd799439088", "name": "Partner Company", "slug": "partner-company" },
"subCompany": null
}
},
"message": "Tokens minted to company"
}Burn Tokens
Remove tokens from a user's balance.
POST /tokens/:tokenId/burnPath Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
tokenId | string | Yes | Token ID |
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
fromUserId | string | Yes | User ID to burn from |
amount | string | Yes | Amount to burn |
memo | string | No | Transaction memo |
Example Request:
curl -X POST "https://your-domain.com/api/external/v1/tokens/507f1f77bcf86cd799439020/burn" \
-H "X-API-Key: vio_live_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"fromUserId": "507f1f77bcf86cd799439015",
"amount": "500",
"memo": "Redemption for reward"
}'Example Response:
{
"success": true,
"data": {
"_id": "507f1f77bcf86cd799439041",
"tokenId": "507f1f77bcf86cd799439020",
"type": "burn",
"amount": "500",
"fromUserId": "507f1f77bcf86cd799439015",
"memo": "Redemption for reward",
"createdAt": "2024-02-01T12:00:00.000Z"
},
"message": "Tokens burned successfully"
}Adjust User Balance
Send or recall tokens between the company admin balance and a user.
POST /tokens/:tokenId/adjustPath Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
tokenId | string | Yes | Token ID |
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
userId | string | Yes | User ID |
amount | string | Yes | Amount to adjust |
operation | string | Yes | send or recall |
reason | string | Yes | Reason for adjustment |
Example Request:
curl -X POST "https://your-domain.com/api/external/v1/tokens/507f1f77bcf86cd799439020/adjust" \
-H "X-API-Key: vio_live_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"userId": "507f1f77bcf86cd799439015",
"amount": "100",
"operation": "send",
"reason": "Manual adjustment for customer service"
}'Example Response:
{
"success": true,
"data": {
"userBalance": "1100",
"adminBalance": "8900",
"transaction": {
"_id": "507f1f77bcf86cd799439042",
"tokenId": "507f1f77bcf86cd799439020",
"type": "transfer",
"amount": "100"
}
},
"message": "Balance sent"
}Expiry Behavior: When sending tokens, the recipient's lots always inherit the expiry from the source lots (defined at mint time). Expiry dates cannot be overridden during send or recall operations. When recalling tokens, the returned lots inherit the expiry from the user's lots, keeping ledger and sendable balances in sync.
Send Tokens
Send tokens from the company admin balance pool to a user. This is a simplified version of the adjust endpoint with the operation fixed to send.
POST /tokens/:tokenId/sendPath Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
tokenId | string | Yes | Token ID |
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
userId | string | Yes | User ID to send tokens to |
amount | string | Yes | Amount of tokens to send |
reason | string | No | Reason for sending tokens |
Example Request:
curl -X POST "https://your-domain.com/api/external/v1/tokens/507f1f77bcf86cd799439020/send" \
-H "X-API-Key: vio_live_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"userId": "507f1f77bcf86cd799439015",
"amount": "1000",
"reason": "Loyalty reward"
}'Example Response:
{
"success": true,
"data": {
"userBalance": "2000",
"adminBalance": "8000",
"transaction": {
"_id": "507f1f77bcf86cd799439043",
"tokenId": "507f1f77bcf86cd799439020",
"type": "transfer",
"amount": "1000"
}
},
"message": "Tokens sent to user"
}Error Responses:
| Status | Code | Description |
|---|---|---|
| 400 | VALIDATION_ERROR | Insufficient company balance for sending tokens |
| 404 | NOT_FOUND | Token or user not found |
Recall Tokens
Recall tokens from a user back to the company admin balance pool. This is a simplified version of the adjust endpoint with the operation fixed to recall.
POST /tokens/:tokenId/recallPath Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
tokenId | string | Yes | Token ID |
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
userId | string | Yes | User ID to recall tokens from |
amount | string | Yes | Amount of tokens to recall |
reason | string | No | Reason for recalling tokens |
Example Request:
curl -X POST "https://your-domain.com/api/external/v1/tokens/507f1f77bcf86cd799439020/recall" \
-H "X-API-Key: vio_live_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"userId": "507f1f77bcf86cd799439015",
"amount": "500",
"reason": "Balance correction"
}'Example Response:
{
"success": true,
"data": {
"userBalance": "1500",
"adminBalance": "8500",
"transaction": {
"_id": "507f1f77bcf86cd799439044",
"tokenId": "507f1f77bcf86cd799439020",
"type": "transfer",
"amount": "500"
}
},
"message": "Tokens recalled from user"
}Error Responses:
| Status | Code | Description |
|---|---|---|
| 400 | VALIDATION_ERROR | Insufficient user balance for recalling tokens |
| 404 | NOT_FOUND | Token or user not found |
List Token Transactions
Retrieve a list of token transactions.
GET /tokens/transactions/listQuery Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
page | integer | No | Page number (default: 1) |
limit | integer | No | Items per page (default: 20) |
tokenId | string | No | Filter by token ID |
type | string | No | Filter by type: mint, transfer, burn, reward, redeem |
fromDate | datetime | No | Filter from this date |
toDate | datetime | No | Filter until this date |
search | string | No | Search by user name or memo |
Example Request:
curl -X GET "https://your-domain.com/api/external/v1/tokens/transactions/list?type=mint&limit=10" \
-H "X-API-Key: vio_live_your_api_key_here"Example Response:
{
"success": true,
"data": [
{
"_id": "507f1f77bcf86cd799439040",
"tokenId": "507f1f77bcf86cd799439020",
"type": "mint",
"amount": "1000",
"toUserId": "507f1f77bcf86cd799439015",
"memo": "Welcome bonus",
"createdAt": "2024-02-01T12:00:00.000Z",
"token": {
"name": "Loyalty Points",
"symbol": "LP"
},
"toUser": {
"displayName": "John Doe",
"email": "john@example.com"
}
}
],
"pagination": {
"page": 1,
"limit": 10,
"total": 500,
"totalPages": 50,
"hasNextPage": true,
"hasPrevPage": false
}
}Get User Token Balances
Retrieve all token balances for a specific user.
GET /tokens/balance/user/:userIdPath Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
userId | string | Yes | User ID |
Example Request:
curl -X GET "https://your-domain.com/api/external/v1/tokens/balance/user/507f1f77bcf86cd799439015" \
-H "X-API-Key: vio_live_your_api_key_here"Example Response:
{
"success": true,
"data": [
{
"tokenId": "507f1f77bcf86cd799439020",
"tokenName": "Loyalty Points",
"symbol": "LP",
"balance": "1500",
"lockedBalance": "0"
},
{
"tokenId": "507f1f77bcf86cd799439021",
"tokenName": "Reward Coins",
"symbol": "RC",
"balance": "250",
"lockedBalance": "50"
}
]
}10. API Reference: Analytics
Required Scope: analytics
Get Analytics Overview
Get overall tenant analytics including users, vouchers, and transactions.
GET /analytics/overviewExample Request:
curl -X GET "https://your-domain.com/api/external/v1/analytics/overview" \
-H "X-API-Key: vio_live_your_api_key_here"Example Response:
{
"success": true,
"data": {
"users": {
"total": 5000,
"active": 4500,
"newThisMonth": 250
},
"vouchers": {
"totalClaims": 12000,
"redeemed": 8500,
"active": 3500,
"redemptionRate": "70.83%"
},
"transactions": {
"total": 25000,
"thisMonth": 3500
},
"generatedAt": "2024-02-01T12:00:00.000Z"
}
}Get Voucher Analytics
Get detailed voucher analytics for a date range.
GET /analytics/vouchersQuery Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
fromDate | datetime | No | Start date for analytics |
toDate | datetime | No | End date for analytics |
Example Request:
curl -X GET "https://your-domain.com/api/external/v1/analytics/vouchers?fromDate=2024-01-01T00:00:00.000Z&toDate=2024-01-31T23:59:59.000Z" \
-H "X-API-Key: vio_live_your_api_key_here"Example Response:
{
"success": true,
"data": {
"totalVouchers": 50,
"totalClaims": 1200,
"totalRedemptions": 850,
"redemptionRate": "70.83%",
"claimsByDay": [
{ "date": "2024-01-01", "count": 45 },
{ "date": "2024-01-02", "count": 52 }
],
"redemptionsByDay": [
{ "date": "2024-01-01", "count": 30 },
{ "date": "2024-01-02", "count": 38 }
],
"topVouchers": [
{
"voucherId": "507f1f77bcf86cd799439011",
"name": "20% Off Discount",
"claims": 250,
"redemptions": 180
}
]
}
}Get User Analytics
Get detailed user analytics for a date range.
GET /analytics/usersQuery Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
fromDate | datetime | No | Start date for analytics |
toDate | datetime | No | End date for analytics |
Example Request:
curl -X GET "https://your-domain.com/api/external/v1/analytics/users?fromDate=2024-01-01T00:00:00.000Z&toDate=2024-01-31T23:59:59.000Z" \
-H "X-API-Key: vio_live_your_api_key_here"Example Response:
{
"success": true,
"data": {
"summary": {
"totalUsers": 5000,
"newUsersInPeriod": 500,
"activeUsers": 4500
},
"byRole": {
"member": 4800,
"sub_company_admin": 150,
"tenant_admin": 50
},
"dailySignups": [
{ "date": "2024-01-01", "count": 15 },
{ "date": "2024-01-02", "count": 22 }
]
}
}Get Token Analytics
Get detailed token analytics for a date range.
GET /analytics/tokensQuery Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
fromDate | datetime | No | Start date for analytics |
toDate | datetime | No | End date for analytics |
Example Request:
curl -X GET "https://your-domain.com/api/external/v1/analytics/tokens?fromDate=2024-01-01T00:00:00.000Z&toDate=2024-01-31T23:59:59.000Z" \
-H "X-API-Key: vio_live_your_api_key_here"Example Response:
{
"success": true,
"data": {
"summary": {
"totalTransactions": 3500,
"totalMinted": "500000",
"totalBurned": "150000",
"netChange": "350000"
},
"dailyTransactions": [
{ "date": "2024-01-01", "count": 120, "volume": "15000" },
{ "date": "2024-01-02", "count": 145, "volume": "18500" }
],
"byType": {
"mint": 800,
"transfer": 2000,
"burn": 400,
"reward": 200,
"redeem": 100
}
}
}11. Data Models
Voucher
| Field | Type | Constraints / Format | Description |
|---|---|---|---|
_id | string | MongoDB ObjectId (24 hex chars) | Unique identifier |
name | string | Min: 1 char. Trimmed | Voucher display name |
description | string | May be empty "". No max length | Voucher description (plain text) |
value | number | >= 0. Default: 0 | Discount value. Interpretation depends on valueType |
valueType | string | Enum: fixed, percentage. Default: fixed | How value is applied: flat amount or percentage rate |
valueCurrency | string | ISO 4217 code (e.g., THB, HKD, USD). Default: THB | Currency for fixed-value vouchers |
voucherType | string | Enum: cash, discount, product, cash_discount | Voucher business category |
consumptionType | string | Enum: vio_code, coupon_code, url, qr_code, manual, zhichong | Voucher consumption mode |
terms | string | May be empty "". No max length | Terms and conditions |
images | string[] | Each element: valid URL. May be empty [] | Image URLs. First image is primary display |
isActive | boolean | Default: true | Whether voucher is active and claimable |
isTransferable | boolean | Default: false | Whether claimed vouchers can be transferred |
totalQuantity | integer | -1 = unlimited; >= 0 = limited. Default: -1 | Total available for claiming |
claimedQuantity | integer | >= 0. Default: 0 | Number already claimed |
maxClaimsPerUser | integer | 0 = unlimited; >= 1 = limited. Default: 1 | Max claims per individual user |
startDate | string | ISO 8601 datetime. Default: creation time | When voucher becomes valid |
endDate | string | ISO 8601 datetime. null = no expiry | When voucher expires |
visibility | string | Enum: private, public, shared. Default: private | Access scope. Also controls transfer scope between users |
category | string | Enum: Wellness, Health, Food & Beverage, Leisure & Entertainment, Travel & Hospitality, Lifestyle & Services, Others. Nullable | Primary voucher category |
categories | string[] | Array of strings. Each trimmed | Category tags for filtering |
minSpend | number | >= 0. Default: 0 | Minimum purchase amount required. 0 = no minimum |
maxDiscount | number | Must be > 0 when set. Nullable | Max discount cap (useful for percentage vouchers) |
settlementAmount | number | >= 0. Default: 0 | Settlement amount for cross-tenant accounting |
settlementCurrency | string | ISO 4217 code. Default: THB | Settlement currency |
externalProvider | string | Provider code. Nullable | External voucher provider, e.g. vouchain |
externalId | string | Provider-side template ID. Nullable | External voucher template ID |
externalRequiresDirectOrderParams | boolean | Default: false | Whether external redemption requires extra account params |
applicableScope | string | Enum: all_outlets, partial_outlets, single_store | Store applicability scope |
bookingEnabled | boolean | Default: false | Whether booking is enabled |
bookingDaysInAdvance | integer | >= 0. Default: 0 | How many days in advance users can book |
createdAt | string | ISO 8601 datetime. Auto-generated | Creation timestamp |
updatedAt | string | ISO 8601 datetime. Auto-updated | Last update timestamp |
VoucherClaim (UserVoucher)
| Field | Type | Constraints / Format | Description |
|---|---|---|---|
_id | string | MongoDB ObjectId (24 hex chars) | Unique identifier |
voucherId | string | MongoDB ObjectId (24 hex chars) | Associated voucher template ID |
userId | string | MongoDB ObjectId (24 hex chars) | Owner user ID |
status | string | Enum: active, claimed, redeemed, expired. Default: active | Current claim status |
redemptionCode | string | Unique. Auto-generated format: VCH-{base36_timestamp}-{4_random_chars} | Code for redeeming the voucher |
claimedAt | string | ISO 8601 datetime. Default: current time | When the voucher was claimed |
redeemedAt | string | ISO 8601 datetime. null until redeemed | When the voucher was redeemed |
expiresAt | string | ISO 8601 datetime. null = no expiry. Inherited from voucher's endDate | When this claim expires |
redemptionDetails.location | string | Optional | Where the voucher was redeemed |
redemptionDetails.notes | string | Optional | Redemption notes |
redemptionDetails.method | string | Optional | Redemption method, e.g. api_key or pin |
externalVoucherCode | string | Nullable | Fulfilled external voucher code |
externalRedemptionUrl | string | Nullable | Fulfilled external redemption URL |
externalOrderId | string | Nullable | External provider order ID |
externalFulfillmentStatus | string | Enum: pending, processing, fulfilled, failed, null | External fulfillment status |
User
| Field | Type | Constraints / Format | Description |
|---|---|---|---|
_id | string | MongoDB ObjectId (24 hex chars) | Unique identifier |
email | string | Email format. null if not set. At least one of email/phone required | Email address |
phone | string | Phone number format. null if not set | Phone number |
displayName | string | null if not set. Trimmed | Display name |
avatar | string | Valid URL. null if not set | Avatar URL |
role | string | Enum: member, sub_company_admin, tenant_admin, super_admin | User role |
isActive | boolean | Default: true | Active status |
walletAddress | string | Ethereum address format (0x...). Auto-provisioned on creation | Web3 custodial wallet address |
registrationSource | string | Enum: created, direct, store, campaign | How the user was registered |
storeId | object | Populated reference or null. Contains _id (ObjectId), name (string) | Store if registered via store |
campaignId | object | Populated reference or null. Contains _id (ObjectId), name (string), slug (string) | Campaign if registered via campaign |
metadata | object | Key-value pairs. Values can be any type | Custom metadata |
createdAt | string | ISO 8601 datetime. Auto-generated | Registration timestamp |
updatedAt | string | ISO 8601 datetime. Auto-updated | Last update timestamp |
Campaign
| Field | Type | Constraints / Format | Description |
|---|---|---|---|
_id | string | MongoDB ObjectId (24 hex chars) | Unique identifier |
name | string | Min: 1 char. Trimmed | Campaign display name |
description | string | May be empty "". No max length | Campaign description (plain text) |
slug | string | Lowercase, URL-safe. Unique per tenant + sub-company. Auto-generated from name | URL-friendly identifier for campaign pages |
tokenId | string | MongoDB ObjectId (24 hex chars). Required | Token users spend to claim vouchers in this campaign |
isActive | boolean | Default: true | Whether campaign is active. Inactive = hidden from users |
isPublic | boolean | Default: true | Whether campaign is publicly accessible (no login required) |
startDate | string | ISO 8601 datetime. Default: creation time | When the campaign starts |
endDate | string | ISO 8601 datetime. null = no end date | When the campaign ends |
images | string[] | Each element: valid URL. May be empty [] | Campaign banner/cover images |
createdAt | string | ISO 8601 datetime. Auto-generated | Creation timestamp |
Token
| Field | Type | Constraints / Format | Description |
|---|---|---|---|
_id | string | MongoDB ObjectId (24 hex chars) | Unique identifier |
name | string | Trimmed | Token display name |
symbol | string | Typically 2-5 uppercase characters | Token symbol (e.g., LP, RC) |
description | string | May be empty "". No max length | Token description |
totalSupply | string | Numeric string for precision | Total supply |
decimals | integer | >= 0. Default: 0 | Decimal places |
isActive | boolean | Default: true | Active status |
createdAt | string | ISO 8601 datetime. Auto-generated | Creation timestamp |
updatedAt | string | ISO 8601 datetime. Auto-updated | Last update timestamp |
TokenBalance
| Field | Type | Constraints / Format | Description |
|---|---|---|---|
tokenId | string | MongoDB ObjectId (24 hex chars) | Token ID |
tokenName | string | Trimmed | Token display name |
symbol | string | Typically 2-5 uppercase characters | Token symbol |
balance | string | Numeric string for precision. >= "0" | Available (spendable) balance |
lockedBalance | string | Numeric string for precision. >= "0" | Balance locked in pending operations |
Transaction
| Field | Type | Constraints / Format | Description |
|---|---|---|---|
_id | string | MongoDB ObjectId (24 hex chars) | Unique identifier |
tokenId | string | MongoDB ObjectId (24 hex chars) | Token ID |
type | string | Enum: mint, transfer, burn, reward, redeem, expire | Transaction type |
amount | string | Numeric string for precision. > "0" | Transaction amount |
fromUserId | string | MongoDB ObjectId (24 hex chars). null for mint operations | Sender user ID |
toUserId | string | MongoDB ObjectId (24 hex chars). null for burn operations | Recipient user ID |
memo | string | null if not provided. No max length | Transaction memo/reason |
createdAt | string | ISO 8601 datetime. Auto-generated | Transaction timestamp |
Pagination
| Field | Type | Constraints / Format | Description |
|---|---|---|---|
page | integer | >= 1 | Current page number (1-indexed) |
limit | integer | 1 - 100 | Items per page |
total | integer | >= 0 | Total items matching the query |
totalPages | integer | >= 0 | Total pages available |
hasNextPage | boolean | true or false | Whether more pages exist after this |
hasPrevPage | boolean | true or false | Whether pages exist before this |
12. Code Examples
JavaScript/Node.js Example
// VIO API Client Example
const VIO_API_BASE = "https://your-domain.com/api/external/v1";
const API_KEY = "vio_live_your_api_key_here";
// Helper function for API calls
async function vioApi(method, endpoint, body = null) {
const options = {
method,
headers: {
"X-API-Key": API_KEY,
"Content-Type": "application/json",
},
};
if (body) {
options.body = JSON.stringify(body);
}
const response = await fetch(`${VIO_API_BASE}${endpoint}`, options);
const data = await response.json();
if (!data.success) {
throw new Error(data.error?.message || "API request failed");
}
return data;
}
// Example: Create a voucher
async function createVoucher() {
const result = await vioApi("POST", "/vouchers", {
name: "Welcome Discount",
description: "10% off your first purchase",
value: 10,
valueType: "percentage",
totalQuantity: 1000,
maxClaimsPerUser: 1,
visibility: "public",
endDate: "2024-12-31T23:59:59.000Z",
});
console.log("Created voucher:", result.data._id);
return result.data;
}
// Example: Mint tokens to a user
async function mintTokensToUser(tokenId, userId, amount) {
const result = await vioApi("POST", `/tokens/${tokenId}/mint`, {
toUserId: userId,
amount: amount.toString(),
memo: "Reward for purchase",
});
console.log("Minted tokens:", result.data);
return result.data;
}
// Example: Redeem a voucher by code
async function redeemVoucher(redemptionCode, location) {
const result = await vioApi("POST", "/vouchers/redeem-by-code", {
redemptionCode,
location,
notes: "Redeemed at checkout",
});
console.log("Voucher redeemed:", result.data);
return result.data;
}
// Example: Consume a voucher with staff PIN
async function consumeVoucherByPin(redemptionCode, staffPin) {
const result = await vioApi(
"POST",
`/vouchers/redeem/${redemptionCode}/pin`,
{
pin: staffPin, // e.g., 'HA1234'
},
);
console.log("Voucher consumed by:", result.data.redeemedBy);
return result.data;
}
// Example: Get user with their balances
async function getUserWithBalances(userId) {
const [userResult, balancesResult] = await Promise.all([
vioApi("GET", `/users/${userId}`),
vioApi("GET", `/users/${userId}/balances`),
]);
return {
user: userResult.data,
balances: balancesResult.data,
};
}
// Example: Search for a user by email
async function findUserByEmail(email) {
const result = await vioApi(
"GET",
`/users/search/by-identifier?email=${encodeURIComponent(email)}`,
);
return result.data;
}
// Example: Find or create a user (idempotent, handles cross-tenant linking)
async function findOrCreateUser(email, password, displayName) {
const result = await vioApi("POST", "/users/find-or-create", {
email,
password,
displayName,
});
if (result.data.created && result.data.linked) {
console.log("Linked to existing cross-tenant identity");
} else if (result.data.created) {
console.log("Brand new user created");
} else {
console.log("User already exists in this portal");
}
return result.data;
}Python Example
import requests
from typing import Optional, Dict, Any
VIO_API_BASE = 'https://your-domain.com/api/external/v1'
API_KEY = 'vio_live_your_api_key_here'
def vio_api(method: str, endpoint: str, body: Optional[Dict] = None) -> Dict[str, Any]:
"""Make a request to the VIO API."""
headers = {
'X-API-Key': API_KEY,
'Content-Type': 'application/json',
}
url = f'{VIO_API_BASE}{endpoint}'
if method == 'GET':
response = requests.get(url, headers=headers)
elif method == 'POST':
response = requests.post(url, headers=headers, json=body)
elif method == 'PATCH':
response = requests.patch(url, headers=headers, json=body)
elif method == 'DELETE':
response = requests.delete(url, headers=headers)
else:
raise ValueError(f'Unsupported method: {method}')
data = response.json()
if not data.get('success'):
error = data.get('error', {})
raise Exception(error.get('message', 'API request failed'))
return data
# Example: List active vouchers
def list_vouchers(page: int = 1, limit: int = 20):
result = vio_api('GET', f'/vouchers?page={page}&limit={limit}&isActive=true')
return result['data'], result['pagination']
# Example: Create a user
def create_user(email: str, password: str, display_name: str):
result = vio_api('POST', '/users', {
'email': email,
'password': password,
'displayName': display_name,
'role': 'member',
})
return result['data']
# Example: Find or create a user (idempotent, handles cross-tenant linking)
def find_or_create_user(email: str, password: str, display_name: str):
result = vio_api('POST', '/users/find-or-create', {
'email': email,
'password': password,
'displayName': display_name,
})
user = result['data']
if user['created'] and user['linked']:
print('Linked to existing cross-tenant identity')
elif user['created']:
print('Brand new user created')
else:
print('User already exists in this portal')
return user
# Example: Get analytics overview
def get_analytics_overview():
result = vio_api('GET', '/analytics/overview')
return result['data']
# Usage
if __name__ == '__main__':
# List vouchers
vouchers, pagination = list_vouchers()
print(f'Found {pagination["total"]} vouchers')
# Get analytics
analytics = get_analytics_overview()
print(f'Total users: {analytics["users"]["total"]}')Complete Workflow: Create Voucher and Track Claims
#!/bin/bash
# Complete workflow example using cURL
API_BASE="https://your-domain.com/api/external/v1"
API_KEY="vio_live_your_api_key_here"
# 1. Create a new voucher
echo "Creating voucher..."
VOUCHER_RESPONSE=$(curl -s -X POST "$API_BASE/vouchers" \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Flash Sale 25% Off",
"description": "Limited time offer - 25% discount",
"value": 25,
"valueType": "percentage",
"totalQuantity": 100,
"maxClaimsPerUser": 1,
"visibility": "public",
"startDate": "2024-02-01T00:00:00.000Z",
"endDate": "2024-02-28T23:59:59.000Z"
}')
VOUCHER_ID=$(echo $VOUCHER_RESPONSE | jq -r '.data._id')
echo "Created voucher: $VOUCHER_ID"
# 2. Check voucher details
echo "Fetching voucher details..."
curl -s -X GET "$API_BASE/vouchers/$VOUCHER_ID" \
-H "X-API-Key: $API_KEY" | jq
# 3. List claims for this voucher
echo "Listing claims..."
curl -s -X GET "$API_BASE/vouchers/claims/list?voucherId=$VOUCHER_ID" \
-H "X-API-Key: $API_KEY" | jq
# 4. When a customer presents a redemption code
REDEMPTION_CODE="ABC123XYZ"
echo "Looking up redemption code..."
curl -s -X GET "$API_BASE/vouchers/redeem/$REDEMPTION_CODE/info" \
-H "X-API-Key: $API_KEY" | jq
# 5. Redeem the voucher
echo "Redeeming voucher..."
curl -s -X POST "$API_BASE/vouchers/redeem-by-code" \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"redemptionCode\": \"$REDEMPTION_CODE\",
\"location\": \"Main Store\",
\"notes\": \"Customer order #12345\"
}" | jq
# 6. Check analytics
echo "Fetching voucher analytics..."
curl -s -X GET "$API_BASE/analytics/vouchers" \
-H "X-API-Key: $API_KEY" | jq13. Changelog
Version 1.4.0 (June 2026)
Cross-Tenant Identity & Find-or-Create
- New endpoint:
POST /users/find-or-create— idempotent user lookup/creation with automatic cross-tenant linking via GlobalIdentity POST /usersnow scopes deduplication to the current portal (tenant + sub-company) instead of globally — the same email/phone can exist in different portalsPOST /usersnow automatically creates aGlobalIdentityfor new users, enabling cross-tenant SSO- Response includes
createdandlinkedboolean flags to indicate whether the user was newly created and/or linked to an existing cross-tenant identity
Version 1.3.0 (June 2026)
Token-to-Voucher Atomic Exchange
- New endpoint:
POST /vouchers/:voucherId/redeem-with-tokens— atomically deducts tokens and issues a voucher to a user in a single server-to-server call - Requires both
vouchersandtokensscopes on the API key - Supports idempotency via optional
idempotencyKeyparameter to prevent duplicate deductions - Settlement transactions are recorded with
processedByType: api_keyfor clear audit separation from member-initiated claims
Version 1.2.0 (May 2026)
Campaign-aware Voucher Send (Documentation)
- Documented the previously undocumented
POST /vouchers/:voucherId/send-campaignendpoint, which most mini-apps already use to issue vouchers via an auto-selected active campaign quota. - Clarified the response shape difference vs.
POST /vouchers/:voucherId/send: the campaign-aware endpoint exposes the newUserVoucher._idasvoucherNftId(not_id) and does not returnredemptionCode/expiresAt/nftTokenId. - Added a safe extraction snippet that supports both response shapes for integrations that may switch between
/sendand/send-campaign.
Version 1.1.1 (May 2026)
Campaign Voucher Max Qty & Stock Indicators
- Fixed: When the Max Qty toggle is first enabled for a campaign voucher, the quantity now auto-fills with the voucher's current remaining stock (total minus claimed), rather than the initial total quantity
- Once saved, the campaign quantity is fixed and does not auto-decrease as vouchers are redeemed by users
- Added Out of Stock (red) and Low Stock (yellow, ≤ 10 remaining) badges to voucher rows in the Campaign Voucher Management modal and the Vouchers page (both grid and list views)
Version 1.1.0 (April 2026)
Registration Source, Campaign Analytics & PIN Permissions
- Added
registrationSourcefield to User model (created,direct,store,campaign) - Added
storeIdandcampaignIdpopulated references in User responses - Added campaign visit and registration tracking analytics
- New public endpoint:
POST /api/campaigns/:id/track-visitfor campaign page visit tracking - Campaign list now includes
analyticsobject withvisitsandregistrationscounts - Added
permissionsfield to redemption PINs (voucher_redemption,token_claim) - Each PIN can now have one or more permissions controlling what operations it can authorize
POST /api/public/verify-pinresponse now includes apermissionsarray- Voucher consumption by PIN (
POST /vouchers/redeem/:code/pin) now requires the PIN to have thevoucher_redemptionpermission - Existing PINs without explicit permissions default to
voucher_redemptionfor backward compatibility
Version 1.0.0 (February 2024)
Initial Release
- Full CRUD operations for Vouchers
- Full CRUD operations for Users
- Full CRUD operations for Campaigns
- Token management (list, mint, burn, adjust)
- Analytics endpoints for vouchers, users, and tokens
- API key authentication with scopes
- Rate limiting (60/min, 10,000/day)
- IP whitelisting support
Need Help?
- Admin Portal: Create and manage API keys at Settings > API Keys
- Swagger UI: Interactive API documentation at
/api-docs - Support: Contact VIO Support for assistance
This documentation is for VIO External API v1. Last updated: June 2026.