Authentication
Learn how to authenticate with the VPT API using JWT tokens for secure, seamless access to all endpoints.
Overview
The VPT API uses JWT (JSON Web Tokens) for authentication. JWTs provide a secure, stateless way to authenticate requests without storing session data on the server.
Authentication Flow
JWT Authentication
JWT tokens are self-contained and include all necessary information to validate requests. They are signed using the HS256 algorithm to ensure integrity.
Developer Key Access Levels
The developer key determines the level of user information returned during authentication. There are three access levels:
| Access Level | User Fields Returned | Use Case |
|---|---|---|
| SIMPLE |
email, firstname,
lastname, crewcode,
rank, streamingAlias,
streamingActive, virtualAirline,
tokenVersionuserPreferences: defaultUnit,
defaultAircraftOnStartup,
takeoff settings only
|
Default level for basic user identification and minimal data |
| MEDIUM |
All SIMPLE fields plus:
simbriefId, dateJoined,
lastConnected, subscriptionuserPreferences: All settings including settingsLocked,
displayIntegratedTutorial, weather,
performance, export,
simbrief, efb
|
Third-party integrations requiring detailed user profile and subscription data |
| FULL |
Complete user data including all database fields, the full
subscription object (with embedded
preview block), and full userPreferences with
advanced settings
|
Reserved for Virtual Performance Tool's official applications only |
The default developer key (VirtualPerformanceTool)
provides
SIMPLE access level. When applying for a custom
developer key, you can request MEDIUM or
FULL access based on your application's needs.
Contact VPT support with details about your integration to discuss
access level requirements.
Login
To obtain a JWT token, send a POST request to the API with your credentials:
Request Parameters
The request body uses a structured format with query,
devKey, and data at the root level:
| Parameter | Type | Required | Description |
|---|---|---|---|
| query | string | Required | Must be "login" |
| devKey | string | Required | Your application's developer key |
| data.email | string | Required | Your account email address |
| data.password | string | Required | Your account password |
curl -X POST "https://api.virtualperformancetool.com/api" \
-H "Content-Type: application/json" \
-d '{
"query": "login",
"devKey": "your_dev_key",
"data": {
"email": "your_email@example.com",
"password": "your_password"
}
}'
const response = await fetch('https://api.virtualperformancetool.com/api', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
query: 'login',
devKey: 'your_dev_key',
data: {
email: 'your_email@example.com',
password: 'your_password'
}
})
});
const data = await response.json();
if (data.success) {
// Store the token for future requests
const token = data.token;
localStorage.setItem('vpt_token', token);
}
<?php
$ch = curl_init('https://api.virtualperformancetool.com/api');
$data = [
'query' => 'login',
'devKey' => 'your_dev_key',
'data' => [
'email' => 'your_email@example.com',
'password' => 'your_password'
]
];
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($data),
CURLOPT_HTTPHEADER => [
'Content-Type: application/json'
]
]);
$response = curl_exec($ch);
$result = json_decode($response, true);
if ($result['success']) {
$token = $result['token'];
// Store token for future use
$_SESSION['vpt_token'] = $token;
}
import requests
response = requests.post(
'https://api.virtualperformancetool.com/api',
json={
'query': 'login',
'devKey': 'your_dev_key',
'data': {
'email': 'your_email@example.com',
'password': 'your_password'
}
}
)
data = response.json()
if data.get('success'):
token = data['token']
# Store token for future use
Successful Response
{
"success": true,
"apiVersion": "1.3.6",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expires": "2026-02-23T18:59:05+01:00",
"expiresIn": 2592000,
"user": {
"email": "john.doe@example.com",
"firstname": "John",
"lastname": "Doe",
"crewcode": "JD001",
"rank": "CAPT",
"streamingAlias": "LivePro",
"streamingActive": 0,
"virtualAirline": "VPT Airlines",
"tokenVersion": 1,
"subscription": {
"title": "VPT Pro Annual",
"level": 3,
"status": "ACTIVE",
"type": "pro",
"expiry": "2026-12-31T23:59:59+01:00",
"daysRemaining": 250,
"isActive": true,
"isExpired": false,
"isTrial": false,
"isPaused": false,
"pauseUntil": null,
"isAutoRenew": true,
"paymentMethod": "paypal",
"capabilities": {
"isNotam": true,
"isNnc": true,
"isMel": true,
"isCdl": true,
"isCommercial": false,
"isFleetDatabase": true,
"maxCalculation": 1000
},
"preview": {
"active": false,
"reason": null,
"allowedAirports": [],
"message": null
}
},
"userPreferences": {
"defaultUnit": "METRIC",
"defaultAircraftOnStartup": {
"file": "320-212-CFM56-5A3.json",
"userRegistration": null,
"vaFleet": null
},
"takeoff": {
"customizeStandardEngineFailureProcedure": true,
"customizedText": "Maintain [RWY_TRACK]° to 25 NM...",
"displayEngineFailureInFull": true
}
}
}
}
The subscription object
The user.subscription object reflects the current
subscription state for the account. It is included at
MEDIUM and FULL access levels
only. There is one and only one subscription object per user; the
airport restriction (when applicable) is carried by the embedded
preview block.
When the user has no row in the subscriptions table,
the API synthesizes a PREVIEW subscription from the
preview product (currently VPT Pro Annual, with the word
“Annual” rewritten to “Demo” in the
title). The synthesized object is
isActive = true with the embedded
preview.active = true block, so the client can render
the full Pro UI; the API enforces the airport restriction
server-side via preview.allowedAirports.
| Field | Type | Description |
|---|---|---|
title |
string|null | Product title from the products table. For the synthesized PREVIEW subscription the words “Annual”, “Yearly” or “Monthly” are replaced with “Demo” (e.g. VPT Pro Demo). |
level |
int|null | Numeric subscription tier from the product. |
status |
string |
Semantic computed status:
ACTIVE — current and not paused;
ACTIVE_PAUSED — current but renewal is
paused (user retains full access until expiry);
PAUSED — renewal paused and
expiry has passed (gap period, no access);
EXPIRED — lapsed normally;
CANCELLED — explicitly cancelled. The
synthesized PREVIEW subscription always reports
ACTIVE with preview.active = true.
|
type |
string |
Lightweight tag from the users row (e.g. free,
pro).
|
expiry |
string|null |
ISO 8601 date when the current subscription period ends.
null for the synthesized PREVIEW subscription.
|
daysRemaining |
int|null |
Whole days until expiry (negative if expired).
null for the synthesized PREVIEW subscription.
|
isActive |
bool |
True when date_end is in the future (regardless
of status — a cancelled subscription continues to run until
its end date). Always true for the synthesized
PREVIEW subscription.
|
isExpired |
bool |
True when date_end is in the past (regardless of
status).
|
isTrial |
bool | True for short Apple Pay introductory periods (≤ 14 days). |
isPaused |
bool |
True when the subscription renewal is currently paused (status
is ACTIVE_PAUSED or PAUSED).
|
pauseUntil |
string|null |
ISO 8601 date when billing is scheduled to resume.
null when the subscription is not paused. Present
in both ACTIVE_PAUSED (renewal delayed; access
still active) and PAUSED (gap period; preview
mode active) states.
|
isAutoRenew |
bool | True when the subscription will renew automatically. |
paymentMethod |
string|null |
paypal | 2checkout |
applepay | commercial.
null for the synthesized PREVIEW subscription.
|
capabilities.isNotam |
bool | NOTAM data accessible. |
capabilities.isNnc |
bool | Non-Normal Checklists accessible. |
capabilities.isMel |
bool | Minimum Equipment List accessible. |
capabilities.isCdl |
bool | Configuration Deviation List accessible. |
capabilities.isCommercial |
bool | Commercial-use license granted. |
capabilities.isFleetDatabase |
bool | Full fleet database accessible. |
capabilities.maxCalculation |
int|null | Maximum calculations per period (null = unlimited). |
preview |
object | Embedded preview-mode descriptor. See below. |
The embedded subscription.preview block
Every subscription object carries an embedded
preview block. When preview.active is
true the user is granted the full Pro UI but the API
only accepts requests against the airports listed in
preview.allowedAirports; requests against any other
ICAO are rejected server-side.
preview.active is true when:
- The user has no real subscription row (synthesized PREVIEW subscription from product id 3, VPT Pro Demo).
-
The user's subscription
date_endis in the past.
For real, currently-active subscriptions (including commercial
subscriptions manually granted with no payment gateway),
preview.active is false,
allowedAirports is empty and the user has unrestricted
access.
| Field | Type | Description |
|---|---|---|
active |
bool | True when the user is in preview mode. |
reason |
string|null |
no_subscription (no row in the subscriptions
table) | expired (row exists but
date_end is in the past), or
null when not active.
|
allowedAirports |
string[] |
ICAO codes the user can run calculations against while in
preview mode. Empty when active is
false.
|
message |
string|null |
Human-readable label suitable for a UI badge.
null when active is
false.
|
When subscription.preview.active is
true: show a “PREVIEW” badge, and
pre-validate user-entered ICAOs against
preview.allowedAirports before issuing calculation
requests. Any other ICAO will be rejected by the server with an
authorization error.
MEDIUM Access Level includes additional fields:
simbriefId- SimBrief account IDdateJoined- Account creation timestamplastConnected- Last login timestamp-
Extended
userPreferencesincluding:settingsLocked,displayIntegratedTutorial,weather,performance,export,simbrief,efbgroups
The exact fields returned in the user object depend
on your developer key's access level:
- SIMPLE (Default): 9 basic fields + limited userPreferences (defaultUnit, defaultAircraftOnStartup, takeoff)
- MEDIUM: All SIMPLE fields + id, simbriefId, dateJoined, lastConnected + full userPreferences (excluding admin settings)
- FULL: Complete database record including admin and internal fields (VPT apps only)
See Developer Key Access Levels section above for detailed field lists.
Error Response
{
"success": false,
"error": {
"message": "Invalid email or password.",
"code": "AUTH_INVALID_CREDENTIALS",
"status": 401
}
}
Using Tokens
Once you have a JWT token, include it in the
Authorization header of all API requests using the
Bearer scheme:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Example: Protected Request
curl -X POST "https://api.virtualperformancetool.com/api" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"query": "performance",
"data": { [...] }
}'
Store your JWT token securely. For web applications, consider
using
httpOnly cookies or secure local storage. Never
expose tokens in URLs or client-side code.
Token Refresh (Sliding Expiration)
To provide a seamless experience, the API implements sliding expiration. When a token is used and has less than 7 days remaining before expiration, a new token is automatically issued.
How It Works
- Token is valid for 30 days from issuance
- When token has < 7 days remaining and a request is made:
- The API returns a new token in the response
- Both old and new tokens remain valid until expiration
- Your application should store and use the new token
Response with Refreshed Token
When your token is refreshed, the response includes a new token:
{
"success": true,
"data": {
// ... your requested data
},
"token_refreshed": true,
"new_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expiresIn": 2592000
}
Handling Refreshed Tokens
async function apiRequest(endpoint, data) {
const token = localStorage.getItem('vpt_token');
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
const result = await response.json();
// Check if token was refreshed
if (result.token_refreshed && result.new_token) {
localStorage.setItem('vpt_token', result.new_token);
console.log('Token refreshed automatically');
}
return result;
}
With sliding expiration, users who are regularly active will never need to re-authenticate. The token automatically extends as long as they use the API at least once every 30 days.
Token Revocation
In case of account compromise, all existing tokens can be invalidated by changing the account password. This is implemented using a token version mechanism.
How Token Revocation Works
- Each JWT contains a version number (
vclaim) - The user's current token version is stored in the database
- On each request, the token version is verified against the database
- When password is changed, the token version is incremented
- All tokens with older versions become invalid
Changing your password will invalidate all existing tokens. All devices and applications will need to re-authenticate. You can also explicitly invalidate all tokens using the Logout All Devices endpoint.
Handling Revoked Tokens
When a token has been revoked, the API returns:
{
"success": false,
"error": "Token has been revoked",
"error_code": "AUTH_TOKEN_REVOKED"
}
Your application should handle this by prompting the user to log in again:
async function apiRequest(endpoint, data) {
const response = await fetch(endpoint, { /* ... */ });
if (response.status === 401) {
const error = await response.json();
if (error.error_code === 'AUTH_TOKEN_REVOKED') {
// Clear stored token
localStorage.removeItem('vpt_token');
// Redirect to login
window.location.href = '/login?reason=token_revoked';
return;
}
}
return response.json();
}
Logout All Devices
Invalidate all existing JWT tokens across every device and session. This uses the same token version mechanism as password changes, but without requiring a password change. A new token is returned so the current device stays logged in.
Request Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| query | string | Required | Must be "logoutAll" |
Request Example
curl -X POST "https://api.virtualperformancetool.com/api" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"query": "logoutAll"
}'
const response = await fetch('https://api.virtualperformancetool.com/api', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
query: 'logoutAll'
})
});
const data = await response.json();
if (data.success) {
// Replace stored token with the new one
localStorage.setItem('vpt_token', data.token);
console.log('Logged out from all other devices');
}
Successful Response
{
"success": true,
"message": "Successfully logged out from all other devices.",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expires": "2026-04-20T18:59:05+01:00",
"expiresIn": 2592000
}
Response Fields
| Field | Type | Description |
|---|---|---|
token |
string |
A new JWT token for the current session. All previous tokens are now invalid. |
expires |
string |
ISO 8601 expiration date of the new token. |
expiresIn |
integer |
Token lifetime in seconds (2592000 = 30 days). |
After calling logoutAll, you
must replace your stored token with the new one
returned in the response. The old token is no longer valid.
Next Steps
Now that you understand authentication, you're ready to: