OFP Sharing API
Cross-account flight plan sharing. Lets an OFP owner invite another VPT account — by email, by short share-code, or by QR deep-link — to view or co-edit one of their stored flights and its Flight Data.
Overview
Sharing is built on a single table, ofp_shares. Every
invite gets two identifiers:
- shareToken — a 48-character URL-safe opaque token. Used inside the QR payload and deep-link URL. Sensitive: only echoed back to the owner.
-
shareCode — an 8-character uppercase
alphanumeric code (alphabet excludes look-alikes
I/O/0/1) for recipients who can’t scan the QR.
A share has a status lifecycle of
pending → accepted (or
revoked at any time). It may carry an
expiresAt after which it can no longer be redeemed even
if still pending.
Permission model
Access checks for OFP reads, Flight Data reads, and Flight Data
writes all go through the
OfpAccessService. It returns the OWNER’s userId
for every authorized caller — that’s how shared users
read and write the same underlying Flight Data row.
| Permission | Can do |
|---|---|
view |
Read the OFP (ofpGet), list/read Flight Data
(flightDataList, flightDataGet).
|
edit |
Everything view can do, plus
flightDataSave and flightDataDelete.
|
Deleting the OFP itself (ofpDelete) and creating /
revoking shares stay restricted to the owner regardless of the
recipient’s permission.
QR code & deep link
The server does not render the QR image — the
client does, from the qrPayload string returned by
ofpShare and ofpShareList. Both the
qrPayload and deepLink use the
vpt://share/{shareToken} scheme so the mobile app can
register a single deep-link handler that routes straight to
ofpShareAccept:
vpt://share/abcDEF-48charsTokenHere
https://www.virtualperformancetool.com/share?t=abcDEF-48charsTokenHere
ofpShare
Owner-only. Creates a new pending invite, returns the token, short share-code and QR payload.
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
query |
string | Required | Must be "ofpShare" |
data.ofpId |
string | Required | OFP identifier (caller must own it) |
data.permission |
string | Optional |
"view" (default) or "edit"
|
data.sharedWithEmail |
string | Optional | Email of the invited account. The invite is still issued if the email isn’t a registered VPT user yet — the recipient redeems it via shareCode/QR after sign-up. |
data.expiresInDays |
integer | Optional | 1–365. Omit / null for no expiry. |
Example
curl -X POST "https://api.virtualperformancetool.com/api" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"query": "ofpShare",
"data": {
"ofpId": "12345_4",
"permission": "edit",
"sharedWithEmail": "copilot@example.com",
"expiresInDays": 7
}
}'
Response
{
"success": true,
"share": {
"id": 42,
"ownerUserId": 9576,
"ofpId": "12345_4",
"sharedWithUserId": 11203,
"sharedWithEmail": "copilot@example.com",
"permission": "edit",
"status": "pending",
"shareCode": "K7H2PXQR",
"createdAt": "2026-05-27 20:14:11",
"acceptedAt": null,
"revokedAt": null,
"expiresAt": "2026-06-03 20:14:11",
"shareToken": "v9Nq...48charsTotal...wA",
"shareUrl": "https://www.virtualperformancetool.com/share?t=v9Nq...wA",
"deepLink": "vpt://share/v9Nq...wA",
"qrPayload": "vpt://share/v9Nq...wA"
}
}
ofpShareList
List shares related to the caller. Returns two arrays:
issued (shares the caller created — include the
shareToken so the owner can re-render the QR) and
received (shares targeted at the caller’s userId
or email — never include the shareToken).
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
query |
string | Required | Must be "ofpShareList" |
data.ofpId |
string | Optional | Filter to a single OFP |
data.scope |
string | Optional |
"all" (default), "issued", or
"received"
|
ofpShareAccept
The recipient redeems an invite using either the
shareToken (deep-link / QR path) or the human-typed
shareCode. Idempotent: re-accepting an already-
accepted share returns success with already: true.
Request Body (one of)
| Parameter | Type | Description |
|---|---|---|
data.shareToken |
string | 48-char token from the QR payload / deep link |
data.shareCode |
string | 8-char human-typed code (case-insensitive) |
Example
curl -X POST "https://api.virtualperformancetool.com/api" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"query": "ofpShareAccept",
"data": { "shareCode": "K7H2PXQR" }
}'
ofpShareRevoke
Either the owner or the current recipient can revoke. The row is
kept with status = 'revoked' for audit. Idempotent.
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
query |
string | Required | Must be "ofpShareRevoke" |
data.id |
integer | Required | Share id from ofpShareList |
Error codes
| Code | HTTP | Description |
|---|---|---|
AUTH_REQUIRED |
401 | JWT missing or invalid. |
INVALID_OFP_ID |
400 | OFP id missing or contains forbidden characters. |
OFP_NOT_FOUND |
404 |
Caller does not own this OFP (used by ofpShare).
|
INVALID_PERMISSION |
400 |
permission is not "view" or
"edit".
|
INVALID_EMAIL |
400 | sharedWithEmail is not a valid email. |
INVALID_EXPIRY |
400 | expiresInDays outside 1–365. |
INVALID_SHARE_TARGET |
400 | Cannot share with yourself / accept your own share. |
SHARE_NOT_FOUND |
404 | No share matches the supplied token / code / id. |
SHARE_REVOKED |
410 | Share was previously revoked. |
SHARE_EXPIRED |
410 | Share is past its expiresAt. |
SHARE_FORBIDDEN |
403 | This share is bound to a different account / caller can’t revoke it. |
INSUFFICIENT_PERMISSION |
403 |
Caller has view access but tried a Flight Data
write.
|