Subscriptions API

Overview

The Subscriptions API allows authenticated users to retrieve their subscription information including active plans, billing details, payment methods, and subscription features. This replaces the legacy subscription.php endpoint with standard JWT authentication.

Authentication Required

The getSubscriptions endpoint requires a valid JWT token. The user must be logged in, and the developer key used at login must have subscription access enabled. Contact the VPT team to enable this permission on your developer key.

Developer Key Permission

Subscription access is controlled by the subscription permission on your developer key. If your key does not have this permission, you will receive an ACCESS_DENIED error. VPT's own app keys have this enabled by default.

Mutating endpoints (v2)

The endpoints below let an authenticated user manage their own subscription end-to-end. They are dispatched through /api using the query field, exactly like the read endpoints.

Access tiers

Two tiers gate the subscription surface, in addition to the user's JWT and ownership check:

  • READ — driven by the api_dev_keys.subscription column. Allows getSubscriptions, getSubscriptionDetails, getPaymentHistory, getManageSubscriptionUrl.
  • FULL — additional allowlist in api/Subscriptions/access_config.php (per dev key). Allows cancelSubscription, pauseSubscription, resumeSubscription, setAutoRenewal, applyDiscountCode, getCheckoutUrl.

Calls outside the caller's tier return HTTP 403 with SUBSCRIPTION_READ_REQUIRED or SUBSCRIPTION_FULL_REQUIRED.

Endpoint summary

query Tier Purpose
getSubscriptions READ Lists every subscription owned by the caller.
getSubscriptionDetails READ Returns a single subscription in the same shape as getSubscriptions. Use to refresh one subscription after a mutation.
getPaymentHistory READ Merged payment ledger across 2Checkout, PayPal and Apple.
getManageSubscriptionUrl READ Gateway management URL (e.g. Apple's apps.apple.com/account/subscriptions).
cancelSubscription FULL Cancels server-side. Apple subscriptions return APPLE_NOT_SUPPORTED.
pauseSubscription FULL Pauses with a mandatory resumeDate (YYYY-MM-DD).
resumeSubscription FULL Resumes a paused subscription.
setAutoRenewal FULL Toggles auto-renewal (enabled: bool).
applyDiscountCode FULL Validates a code and returns a checkout URL with the discount pre-applied.
getCheckoutUrl FULL Builds a fresh checkout URL for a given product/promo.
changePlan FULL 2CO: change to any plan in the catalogue — prorated or deferred depending on direction. Pending implementation.
paypal2checkoutMigration FULL Transfer remaining days from a PayPal subscription to a new 2CO subscription, with tier-aware bonus days.

Rate limiting

FULL-tier endpoints use the existing subscription_api_logs table for rate limiting. A user may perform at most 5 state-changing actions per hour; requests beyond that limit return HTTP 429 with RATE_LIMITED.

Webhook URLs

The new architecture exposes one public entry per provider-event. These URLs are what you paste into the gateway's control panel.

Provider Event URL
2Checkout Instant Payment Notification /api/webhooks/2checkout/ipn
2Checkout License Change Notification /api/webhooks/2checkout/lcn
PayPal Instant Payment Notification /api/webhooks/paypal/ipn
Apple App Store Server Notifications V2 /api/webhooks/apple/notifications
Apple Mobile-app nudge (StoreKit success) /api/webhooks/apple/mobile
Cutover

Re-pointing webhook URLs is done in each gateway's control panel by VPT staff. The legacy entry points keep working until you swap them out — switching is reversible at any time without code changes.

Get User Subscriptions

POST /api

Returns all subscriptions for the authenticated user, including active, expired (within 30 days), paused, and cancelled plans. Each subscription includes billing details, payment method information, and available features.

Request Parameters

Parameter Type Required Description
query string Required Must be "getSubscriptions"

Request Body

JSON
{
  "query": "getSubscriptions"
}

cURL Example

cURL
curl -X POST "https://api.virtualperformancetool.com/api" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_JWT_TOKEN" \
  -H "X-API-Version: 1.3.6" \
  -d '{
    "query": "getSubscriptions"
  }'

Response

JSON
{
  "apiVersion": "1.3.6",
  "success": true,
  "subscriptions": [
    {
      "subscriptionId": 1234,
      "subscriptionReference": "ABC123DEF456",
      "title": "VPT 737NG Pro Yearly",
      "groupPackage": "VPT_GLOBAL_Pro",
      "paymentMethod": "2checkout",
      "status": "ACTIVE",
      "isPaused": false,
      "pauseUntil": null,
      "pauseReason": null,
      "autoRenew": true,
      "billing": {
        "frequency": "yearly",
        "interval": "Y",
        "pricePaid": 49.99,
        "currency": "USD",
        "nextBillingDate": "2027-04-15",
        "nextBillingAmount": 49.99,
        "lastPaymentDate": "2026-04-15",
        "cyclesCompleted": null
      },
      "subscriber": {
        "email": null,
        "name": null,
        "cardLast4": "1111"
      },
      "dates": {
        "start": "2026-04-15",
        "end": "2027-04-15",
        "daysRemaining": 274,
        "isActive": true,
        "isExpired": false
      },
      "image": {
        "filename": "VPT_GLOBAL_Pro.jpg",
        "url": "https://api.virtualperformancetool.com/assets/images/subscriptions/VPT_GLOBAL_Pro.jpg"
      },
      "actions": {
        "canPause": true,
        "canCancel": true,
        "canUpgrade": true,
        "canRenew": true,
        "renewalUrl": "https://secure.2checkout.com/renewal/?LICENSE=ABC123DEF456",
        "appleManageUrl": null
      },
      "pricing": {
        "catalogPrice": 79.99,
        "discountPercent": 18,
        "promoCode": "VA-ALLIANCE-18",
        "discountRecurring": true,
        "discountMaxCycles": 3,
        "durationDays": 365,
        "durationLabel": "1 year"
      },
      "features": {
        "level": 3,
        "maxCalculation": 999,
        "isNotam": true,
        "isNnc": true,
        "isMel": true,
        "isCdl": true,
        "isMostFav": true,
        "isBalkedLanding": true,
        "isAllIntersections": true
      }
    }
  ],
  "totalCount": 1
}

Response Fields

Field Type Description
success boolean Whether the request was successful
subscriptions array Array of subscription objects (see below). Empty array if the user has no subscriptions.
totalCount integer Total number of subscriptions returned

Subscription Object

Each subscription in the subscriptions array contains:

Core Fields

Field Type Description
subscriptionId integer Unique subscription identifier
subscriptionReference string|null External payment provider subscription reference
title string Product display name
groupPackage string|null Product group identifier
paymentMethod string Payment method type: paypal, 2checkout, applepay, commercial, unknown
status string Semantic subscription status: ACTIVE — current and not paused; ACTIVE_PAUSED — current but renewal is paused (user retains full access until end date); PAUSED — renewal paused and end date has passed (gap period, no access); EXPIRED — lapsed normally; CANCELLED — explicitly cancelled.
isPaused boolean Whether 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.
pauseReason string|null Reason provided when the subscription was paused, or null
autoRenew boolean Whether auto-renewal is active

Billing Object

Field Type Description
billing.frequency string monthly, yearly, or unknown
billing.interval string Raw interval code: Y (yearly) or M (monthly)
billing.pricePaid float Original purchase price for the current period (rounded to 2 d.p.)
billing.currency string|null ISO 4217 currency code (e.g. EUR, USD). Populated live from 2Checkout; null for PayPal, Apple Pay, and commercial subscriptions.
billing.nextBillingDate string|null ISO 8601 next billing date. Populated live from 2Checkout; null for non-2CO subscriptions.
billing.nextBillingAmount float|null Gross renewal amount including VAT. Populated live from 2Checkout; null for non-2CO subscriptions.
billing.lastPaymentDate string|null Date of the most recent payment, or null if unavailable
billing.cyclesCompleted integer|null Number of billing cycles completed, or null if unavailable

Dates Object

Field Type Description
dates.start string Subscription start date
dates.end string Subscription end date
dates.daysRemaining integer Days remaining until expiration
dates.isActive boolean Whether the subscription is within its active date range
dates.isExpired boolean Whether the subscription end date has passed

Features Object

Field Type Description
features.level integer Subscription level (determines feature access)
features.maxCalculation integer Maximum calculations allowed per billing period
features.isNotam boolean Whether NOTAM data access is included
features.isNnc boolean Whether NNC (Non-Normal Checklist) access is included
features.isMel boolean Whether MEL (Minimum Equipment List) access is included
features.isCdl boolean Whether CDL (Configuration Deviation List) access is included
features.isMostFav boolean Whether the most-favourable runway/condition optimisation is included (VPT Global Pro)
features.isBalkedLanding boolean Whether the balked-landing (go-around) procedure calculation is included (VPT Global Pro)
features.isAllIntersections boolean Whether the all-intersections takeoff calculation is included (VPT Global Pro)

Actions Object

Field Type Description
actions.canPause boolean Whether the subscription can be paused (2CO and PayPal only)
actions.canCancel boolean Whether the subscription can be cancelled
actions.canUpgrade boolean Whether a plan change is available (use changePlan endpoint)
actions.canRenew boolean Whether the subscription can be manually renewed
actions.renewalUrl string|null Direct 2CO manual renewal URL (null for non-2CO subscriptions). Open in WebView — no VPT API endpoint for renewal.
actions.appleManageUrl string|null Apple Pay subscriptions only: deep-link to apple.com/account/subscriptions. null for all other payment methods. All other actions (cancel, pause, upgrade, manage billing) go through VPT API endpoints — no embedded URLs.
pricing.catalogPrice number|null Full list price from the product catalog, before any discount. Use as the strikethrough price in the UI (e.g. ~~€79.99~~) when a discount was applied.
pricing.discountPercent integer|null Percentage discount applied at the time of purchase. null if no discount code was used or the code is not tracked in the discount_codes table.
pricing.promoCode string|null The discount / promo code used at purchase. null if none was used.
pricing.discountRecurring boolean|null true — discount applies to renewal cycles (apply_recurring = ALL).
false — discount was first payment only (apply_recurring = NONE).
null — no discount code was used.
pricing.discountMaxCycles integer|null Number of renewal cycles the discount is limited to (maps to 2CO RecurringChargesNumber).
null — unlimited renewals at the discounted price (or no discount / first-payment-only).
N — discount applies to the first N renewals only. Only meaningful when discountRecurring === true.
pricing.durationDays integer|null Subscription length in days as defined in the product catalog.
pricing.durationLabel string|null Human-readable subscription duration, e.g. "1 year", "1 month", "2 years". Based on paypal_interval and paypal_interval_count from the product catalog.

Flutter / Mobile API (subscription.php)

The VPT Flutter app uses a dedicated HMAC-signed endpoint (subscription.php) that is separate from the JWT-based /api endpoint documented above. All actions in this section are dispatched through this endpoint.

Base URL

HTTP
GET  https://api.virtualperformancetool.com/subscription.php?action=ACTION
POST https://api.virtualperformancetool.com/subscription.php?action=ACTION

Request Headers

Header Required Description
X-API-Key Required The user's API key.
X-Timestamp Required Unix timestamp in seconds. Must be within ±5 minutes of server time.
X-Signature Required HMAC-SHA256 request signature (see algorithm below).
Content-Type POST only application/json

Signature Algorithm

PHP
$secret    = hash('sha256', $userId . SAFETY_CODE);
$message   = $apiKey . $timestamp . $action;
$signature = hash_hmac('sha256', $message, $secret);

Confirmation Tokens (State-Changing Actions)

Every POST action that modifies subscription state requires a confirmation_token in the JSON body. The token is generated client-side and is valid for 10 minutes.

PHP
// Build a confirmation token (requires SAFETY_CODE)
$timestamp = time();
$data  = $action . '_' . ($subscriptionId ?? 'general') . '_' . $timestamp . '_' . $userId;
$hash  = hash_hmac('sha256', $data, SAFETY_CODE);
$token = base64_encode(json_encode([
    'action'          => $action,
    'subscription_id' => $subscriptionId,
    'timestamp'       => $timestamp,
    'user_id'         => $userId,
    'hash'            => $hash,
]));
Rate Limit

State-changing actions are limited to 5 per user per hour. Exceeding the limit returns HTTP 429 with RATE_LIMITED.

Action Summary

action Method Description
user_subscriptions GET List all user subscriptions.
subscription_details GET Single subscription with live gateway status.
payment_history GET Paginated payment ledger.
pause_subscription POST ★ Pause (PayPal & 2CO; Apple → 422).
resume_subscription POST ★ Resume a paused subscription.
cancel_subscription POST ★ Cancel immediately.
change_plan POST ★ 2CO: change plan — upgrades prorated by gateway, downgrades re-prorated server-side (extends date_end).
change_payment_method POST ★ 2CO: get hosted payment-update URL.
request_refund POST ★ 2CO: initiate refund (currently admin-only).
migration_preview GET Check PayPal → 2CO migration eligibility.
migration_start POST ★ Create migration intent + checkout URL.
migration_status GET Poll migration completion.
migration_cancel POST ★ Discard a pending migration intent.

★ = state-changing; requires confirmation_token in body; counts against the 5/h rate limit.

Gateway Capabilities

Not every payment gateway supports server-side management. When an unsupported operation is attempted the API returns HTTP 422 with error code OPERATION_NOT_SUPPORTED and a manage_url field pointing to the provider's self-service portal.

Operation PayPal 2Checkout Apple IAP
Pause ✅ NVP / REST fallback ✅ ❌ → App Store
Resume ✅ NVP / REST fallback ✅ ❌ → App Store
Cancel ✅ NVP / REST fallback ✅ ❌ → App Store
Upgrade ❌ → PayPal ✅ prorated immediately ❌ → App Store
Downgrade ❌ → PayPal ✅ effective next renewal ❌ → App Store
Refund ❌ (webhook-inbound only) ✅ ❌ (webhook-inbound only)
Change Payment Method ❌ → PayPal ✅ hosted URL ❌ → App Store
422 OPERATION_NOT_SUPPORTED

Responses for unsupported operations include a manage_url string that the app should open in a webview so the user can manage the subscription directly with the provider.

Get Payment History

GET /subscription.php?action=payment_history

Returns the paginated payment ledger for the authenticated user. When filtering by subscription_id for a 2Checkout subscription, live order data is fetched directly from the 2CO API and falls back to the local IPN log table if the gateway call fails.

Query Parameters

Parameter Type Default Description
limit integer 50 Records per page (1–100).
offset integer 0 Zero-based page offset for pagination.
subscription_id integer — Filter results to a single subscription.
from string — Start date filter YYYY-MM-DD.
to string — End date filter YYYY-MM-DD.

Response

JSON
{
  "success": true,
  "message": "Payment history retrieved successfully",
  "user_id": 42,
  "payments": [
    {
      "id": 101,
      "subscription_id": 7,
      "amount": 49.99,
      "currency": "EUR",
      "payment_method": "2checkout",
      "payment_date": "2026-04-15 10:23:00",
      "status": "complete",
      "transaction_id": "104815628",
      "gateway": "2checkout"
    }
  ],
  "total_count": 6,
  "limit": 50,
  "offset": 0,
  "has_more": false
}

Pause / Resume / Cancel

All three actions update the local subscription status and synchronise the change to the payment gateway where supported (see Gateway Capabilities). For PayPal subscriptions the NVP API is tried first and the REST API is used as a fallback. A gateway_warning field is included in the response if the gateway sync fails — the local status is still updated in that case.

Pause Subscription

POST /subscription.php?action=pause_subscription

Body Parameters

Field Type Required Description
confirmation_token string Required Action-scoped HMAC token.
subscription_id integer Required Subscription to pause.
resume_date string Optional 2CO only: scheduled resume date YYYY-MM-DD HH:MM:SS. Defaults to 30 days from now.

Response

JSON
{ "success": true, "subscription_id": 7, "previous_status": "Running", "new_status": "paused" }

Resume Subscription

POST /subscription.php?action=resume_subscription

Body Parameters

Field Type Required Description
confirmation_token string Required
subscription_id integer Required

Response

JSON
{ "success": true, "subscription_id": 7, "previous_status": "paused", "new_status": "Running" }

Cancel Subscription

POST /subscription.php?action=cancel_subscription

Body Parameters

Field Type Required Description
confirmation_token string Required
subscription_id integer Required
reason string Optional Human-readable cancellation reason (logged internally).

Response

JSON
{ "success": true, "subscription_id": 7, "previous_status": "Running", "new_status": "Cancelled" }

Change Plan (2Checkout only)

Change the product on an active 2Checkout subscription. Moves to any plan in the catalogue — upgrades are prorated at the gateway (price difference charged immediately) while downgrades are re-prorated server-side so the unused value carries over as extra days at the new daily rate (see Plan Change Server Behaviour). Validate the target plan against the tier hierarchy below.

changePlan (v2 API) — Pending Implementation

The changePlan query on the /api endpoint is not yet available. Plan changes are currently handled through the subscription.php Mobile API (change_plan action documented below).

Product Tier Hierarchy

Rank Product Code Plan
1 VPT_73NG_LITE_M Lite — Monthly
2 VPT_73NG_LITE_Y Lite — Yearly
3 VPT_73NG_STANDARD_M Standard — Monthly
4 VPT_73NG_STANDARD_Y Standard — Yearly
5 VPT_73NG_PRO_M Pro — Monthly
6 VPT_73NG_PRO_Y Pro — Yearly

Change Plan (Mobile API)

POST /subscription.php?action=change_plan

Changes the product on an active 2CO subscription. Upgrades to a higher-ranked product are prorated and charged immediately. Downgrades to a lower-ranked product are not charged but the remaining paid value is re-prorated and added as extra days at the new (cheaper) daily rate — see Plan Change Server Behaviour.

Body Parameters

Field Type Required Description
confirmation_token string Required Action-scoped HMAC token.
subscription_id integer Required 2CO subscription to change.
target_pcode string Required Target 2CO product code (e.g. VPT_73NG_PRO_Y). Must differ from current plan.

Response

JSON
// Upgrade (prorated charge applied immediately):
{
  "success": true,
  "subscription_id": 7,
  "previous_pcode": "VPT_73NG_STANDARD_M",
  "new_pcode": "VPT_73NG_PRO_Y",
  "note": "Price difference has been prorated and will be charged immediately."
}

// Downgrade (no charge, date_end extended by re-prorating remaining value):
{
  "success": true,
  "subscription_id": 7,
  "previous_pcode": "VPT_73NG_PRO_Y",
  "new_pcode": "VPT_73NG_STANDARD_Y",
  "note": "Downgrade applied. Remaining paid value re-prorated at the new daily rate; date_end has been extended."
}

Plan Change Server Behaviour

Independently of which client triggers a plan change (Mobile API, 2Checkout Customer Area, or admin tooling), the server-side IPN/LCN dispatcher enforces the following rules so the subscriptions table stays consistent:

  • One active row per user. Whenever a CREATE or plan-change event lands a row for user X, every other row of X with status Running or Paused is automatically moved to Superseded. This fixes the historical bug where an upgrade from Lite → Pro would leave the old Lite row active.
  • Upgrades trust the gateway date. 2Checkout charges the price difference at the moment of the upgrade and supplies a new LICENSE_EXP_DATE; we copy it verbatim onto the (kept) subscription row.
  • Downgrades are re-prorated server-side. 2Checkout does NOT refund the unused portion of the more expensive plan. To honour the value already paid we extend date_end using:
    remainingDays   = max(0, oldDateEnd - today)
    remainingValue  = remainingDays / oldDurationDays × oldPricePaid
    newDailyRate   = newPrice / newDurationDays
    extendedDays   = floor(remainingValue / newDailyRate)   // capped at 1095 d
    newDateEnd     = today + extendedDays
    Worked example — user on Pro-Y (100 €, 365 d) downgrades to Lite-Y (50 €, 365 d) with 339 days remaining:
    remainingValue = 339/365 × 100 ≈ 92.88 €, newDailyRate = 50/365 ≈ 0.137 €/d, extendedDays ≈ 678 d ⇒ roughly two extra years on Lite, as expected.
  • Lateral changes (same tier, different billing cycle) transfer the remaining days 1:1.
  • LCN plan detection. 2Checkout downgrades typically arrive only as an LCN with STATUS=ACTIVE and a changed NEXT_RENEWAL_PRICE — the LCN payload does not carry the new PCODE. The dispatcher detects the price change, calls GET /subscriptions/{ref}/ on 2Checkout to read the current PCODE, resolves it to a local products.id, and reroutes the event through the plan-change handler.

2Checkout Control Panel prerequisites

Configure both directions

In Setup → Products → Upgrade Options, configure ALL tier pairs in BOTH directions (Lite↔Standard, Lite↔Pro, Standard↔Pro). Without these entries the Customer Area hides the downgrade buttons and Mobile-API change_plan calls fail with "Product is not a configured upgrade target".

  • Enable BOTH webhooks (Setup → System Settings → Notifications): IPN (instant payment notifications) AND LCN (license change notifications). LCN is what carries the downgrade signal.
  • Webhook secrets must match the values stored in config.php (TWOCHECKOUT_*_SECRET).
  • The native 2Checkout proration setting can stay at its default — the server re-computes date_end for downgrades regardless, so flipping it does not change the user-visible outcome.

Change Payment Method (2Checkout only)

POST /subscription.php?action=change_payment_method

Returns a 2Checkout hosted URL where the user can update their saved payment card. Open this URL in an in-app webview. For PayPal and Apple IAP subscriptions the response is HTTP 422 with a manage_url pointing to the provider's portal.

Body Parameters

Field Type Required Description
confirmation_token string Required
subscription_id integer Required

Response

JSON
{
  "success": true,
  "subscription_id": 7,
  "checkout_url": "https://secure.2checkout.com/myaccount/payment-update/I-XXXXXXXXXXXX",
  "instructions": "Open checkout_url in a webview for the user to update their payment details."
}

Request Refund (2Checkout only)

POST /subscription.php?action=request_refund
Admin-Only (Current Release)

User self-serve refunds are currently disabled (REFUND_USER_SELF_SERVE = false). This endpoint always returns HTTP 403 for regular users. Direct refund requests to customer support. Self-serve can be enabled server-side by toggling the constant.

When enabled, initiates a refund for the most-recent 2Checkout order on the subscription. Refunds are processed asynchronously — the gateway fires a REFUND_ISSUED IPN webhook that sets the subscription status to Refunded.

Body Parameters

Field Type Required Description
confirmation_token string Required
subscription_id integer Required
reason string Required One of: FRAUD, DUPLICATE, OTHER, CUSTOMER_REQUEST, QUALITY_ISSUE.
amount float Optional Amount to refund. Omit or pass 0 for a full refund.
comment string Optional Internal note visible in the 2CO dashboard.

Response

JSON
{
  "success": true,
  "subscription_id": 7,
  "order_ref": "104815628",
  "refund_status": "pending",
  "message": "Refund initiated. Status will be confirmed via webhook."
}

PayPal → 2Checkout Migration

Allows a user with an active PayPal subscription to migrate to a 2Checkout subscription, carrying over remaining days with a tier-aware day bonus. Upgrading yields a higher bonus; downgrading transfers days 1:1 (no bonus). The flow is non-destructive: the PayPal subscription is only cancelled once the new 2CO subscription is confirmed by the payment gateway IPN.

Migration Day Bonus by Direction

Direction Example Bonus
Same tier PayPal Pro → 2CO Pro +10 %
Upgrade +1 tier PayPal Standard → 2CO Pro +20 %
Upgrade +2 tiers PayPal Lite → 2CO Pro +30 %
Downgrade PayPal Pro → 2CO Lite 0 % (1:1)

Option A — Direct API (Recommended)

Use the paypal2checkoutMigration query on the standard /api endpoint (FULL tier, JWT auth). This is a two-step process:

  1. Call getCheckoutUrl to generate a purchase URL for the target 2CO plan. Open it in a webview.
  2. After the webview closes, poll getSubscriptions until the new 2CO subscription row appears, then call paypal2checkoutMigration with both IDs.
POST /api — query: paypal2checkoutMigration

Body Parameters

Field Type Required Description
paypalSubscriptionId integer Required DB id of the existing PayPal subscription row.
twocheckoutSubscriptionId integer Required DB id of the newly-purchased 2CO subscription row.

Response

JSON
{
  "success": true,
  "alreadyMigrated": false,
  "remainingDays": 147,
  "bonusDays": 161,
  "newExpiryDate": "2025-09-10 00:00:00",
  "paypalCancelStatus": "ok",   // "ok" | "failed"
  "warning": null
}

The operation is idempotent — repeat calls return alreadyMigrated: true without re-extending. If paypalCancelStatus is "failed", the days were transferred but the PayPal recurring profile could not be cancelled automatically; instruct the user to cancel it manually at paypal.com/myaccount/autopay.

Option B — Mobile API (Legacy Flow)

The subscription.php Mobile API offers a more granular 4-step intent-based flow for apps that need polling.

Flow Overview

  1. Call migration_preview (GET) — check eligibility and receive the checkout URL.
  2. Call migration_start (POST) — create a migration intent; receive intent_id.
  3. Open checkout_url in an in-app webview — user completes the 2CO purchase.
  4. Poll migration_status (GET) with intent_id — wait for "status": "completed".
  5. If user cancels before paying: Call migration_cancel (POST) to discard the intent.
Automatic IPN Linkage

When the 2CO IPN fires after purchase the server automatically runs MigrationService — it cancels the PayPal subscription, credits bonus days, and marks the intent completed. If the IPN processes the migration before the app polls, migration_status will still return completed with the new subscription details.

Step 1 — Migration Preview

GET /subscription.php?action=migration_preview

Read-only. Returns eligibility information and the pre-built 2CO checkout URL. No state is changed.

Query Parameters

Parameter Type Required Description
target_pcode string Optional Override the default same-tier 2CO product code. Accepts any value from MIGRATION_PCODE_MAP (e.g. VPT_73NG_PRO_Y). Omit to use the same-tier default. The available_targets array in the response lists all valid options.

Response — eligible (same-tier, default)

JSON
{
  "success": true,
  "eligible": true,
  "paypal_subscription": {
    "id": 3,
    "date_end": "2026-08-12",
    "product_title": "VPT Standard Yearly"
  },
  "source_product_code": "VPT_73NG_STANDARD_Y",
  "target_product_code": "VPT_73NG_STANDARD_Y",
  "migration_direction": "same",
  "remaining_days": 85,
  "bonus_pct": 10,
  "bonus_extra_days": 9,
  "bonus_days": 94,
  "bonus_description": "Your remaining 85 day(s) will carry over with a 10% bonus (9 extra day(s) added to your new 2Checkout subscription).",
  "checkout_url": "https://secure.2checkout.com/checkout/buy?merchant=XXXX&prod=VPT_73NG_STANDARD_Y&...",
  "available_targets": [
    { "pcode": "VPT_73NG_PRO_Y",      "plan_label": "Pro-Y",      "direction": "upgrade",   "bonus_pct": 20, "bonus_extra_days": 17, "total_days": 102, "is_default": false },
    { "pcode": "VPT_73NG_PRO_M",      "plan_label": "Pro-M",      "direction": "upgrade",   "bonus_pct": 20, "bonus_extra_days": 17, "total_days": 102, "is_default": false },
    { "pcode": "VPT_73NG_STANDARD_Y", "plan_label": "Standard-Y", "direction": "same",      "bonus_pct": 10, "bonus_extra_days": 9,  "total_days": 94,  "is_default": true  },
    { "pcode": "VPT_73NG_STANDARD_M", "plan_label": "Standard-M", "direction": "same",      "bonus_pct": 10, "bonus_extra_days": 9,  "total_days": 94,  "is_default": false },
    { "pcode": "VPT_73NG_LITE_Y",     "plan_label": "Lite-Y",     "direction": "downgrade", "bonus_pct": 0,  "bonus_extra_days": 0,  "total_days": 85,  "is_default": false },
    { "pcode": "VPT_73NG_LITE_M",     "plan_label": "Lite-M",     "direction": "downgrade", "bonus_pct": 0,  "bonus_extra_days": 0,  "total_days": 85,  "is_default": false }
  ],
  "existing_pending_intent_id": null,
  "notes": "Your PayPal subscription will be cancelled automatically after the 2Checkout purchase is confirmed."
}

Response — not eligible

JSON
{
  "success": true,
  "eligible": false,
  "reason": "NO_ELIGIBLE_PAYPAL_SUBSCRIPTION",
  "reason_description": "No active, unmigrated PayPal subscription was found for this account."
}

Step 2 — Migration Start

POST /subscription.php?action=migration_start

Creates (or reuses an existing) migration intent. Returns the checkout URL and an intent_id to track completion. Intents expire after 24 hours.

Body Parameters

Field Type Required Description
confirmation_token string Required Token scoped to migration_start.
subscription_id integer Required The PayPal subscription ID to migrate (must be the eligible one returned by migration_preview).
target_pcode string Optional Override the 2CO product to migrate to (e.g. VPT_73NG_PRO_Y to upgrade during migration). Must be a valid value from MIGRATION_PCODE_MAP. Defaults to the same-tier equivalent when omitted.

Response

JSON
{
  "success": true,
  "intent_id": 14,
  "checkout_url": "https://secure.2checkout.com/checkout/buy?...",
  "expires_at": "2026-05-20 14:35:00",
  "target_product_code": "VPT_73NG_PRO_Y",
  "migration_direction": "upgrade",
  "bonus_pct": 20,
  "bonus_extra_days": 17,
  "bonus_days": 102,
  "instructions": "Open checkout_url in a webview. After purchase, poll migration_status with the intent_id for completion."
}

Step 4 — Migration Status (Polling)

GET /subscription.php?action=migration_status&intent_id=14

Poll this endpoint after the user returns from the checkout webview.

Query Parameters

Parameter Type Required Description
intent_id integer Required The ID returned by migration_start.

Status Values

status Meaning
pending Awaiting IPN confirmation — continue polling.
completed Migration successful. Response includes new_subscription object.
expired 24h TTL elapsed — intent discarded, PayPal subscription unaffected.
failed IPN-side error — contact support.

Response — completed

JSON
{
  "success": true,
  "intent_id": 14,
  "status": "completed",
  "target_product_code": "VPT_73NG_PRO_Y",
  "migration_subscription_id": 22,
  "completed_at": "2026-05-19 14:41:00",
  "new_subscription": {
    "id": 22,
    "date_end": "2027-08-21",
    "status": "Running",
    "product": 5
  }
}

Step 5 — Migration Cancel (Optional)

POST /subscription.php?action=migration_cancel

Marks the migration intent as expired. The original PayPal subscription is not cancelled or modified in any way.

Body Parameters

Field Type Required Description
confirmation_token string Required Token scoped to migration_cancel.
intent_id integer Required ID returned by migration_start.

Response

JSON
{
  "success": true,
  "intent_id": 14,
  "status": "expired",
  "message": "Migration cancelled. Your PayPal subscription is unchanged."
}

Migration from Legacy Endpoint

If your application previously used the standalone subscription.php endpoint with HMAC-SHA256 authentication, here is what changes:

Legacy New API
Endpoint POST /subscription.php?action=user_subscriptions POST /api
Auth HMAC-SHA256 (X-API-Key, X-Timestamp, X-Signature) JWT Bearer token
Query URL parameter: ?action=user_subscriptions Body: {"query": "getSubscriptions"}
Response keys snake_case camelCase
Legacy Endpoint

The legacy subscription.php endpoint remains available for backward compatibility during the transition period. New integrations should use the standard API endpoint documented above.