PayVia Documentation
PayVia is a payments infrastructure for Chrome Extensions and SaaS apps. Add PayPal subscriptions, tier-based feature gating, free trials, and license validation — all with a single JavaScript SDK.
Quick Start
import PayVia from './payvia.js';
const payvia = PayVia('pv_live_xxxxxxxx');
// Check user status (cached, works offline)
const user = await payvia.getUser();
if (user.paid) {
// User has active subscription or trial
console.log('Tier:', user.tier.name); // "Pro"
console.log('Features:', user.features); // ["export_pdf", "api_access"]
}
// Check specific feature
if (await payvia.hasFeature('export_pdf')) {
enableExportButton();
}
// Open payment page
await payvia.openPaymentPage({ mode: 'pricing', email: user.email });Getting Started
- 1Create a PayVia account
Sign up for free at PayVia and log in to the dashboard.
- 2Create a Project
Each project represents one app or extension. You'll get an API key for it.
- 3Set up Tiers
Create access tiers like Free, Pro, Super. Each tier has a numeric level and a list of features (e.g., "export_pdf", "api_access").
- 4Create Plans within Tiers
Add pricing options to each tier: Monthly ($9.99/mo), Yearly ($79.99/yr), or Lifetime ($199 once).
- 5Configure PayPal credentials
Connect your PayPal Business account. Recurring plans (Monthly/Yearly) are auto-synced to PayPal Billing Plans.
- 6Enable Auto Trial (optional)
Let new users start a free trial automatically. Configure trial duration (1–90 days) and which tier to grant.
- 7Integrate the SDK
Install @payvia-sdk/sdk (or copy payvia.js). Initialize with your API key. Call getUser() to check license status.
- 8Start accepting payments
Use openPaymentPage() to redirect users to PayPal checkout. PayVia handles webhooks and license activation.
Concepts
Project
Your app or extension. Each project gets its own API key, plans, tiers, and subscriber list.
Tier
A feature-based access level (e.g., Free, Pro, Super). Has a numeric level (0, 1, 2) for ordering and a list of feature strings. Plans belong to tiers.
Plan
A pricing option within a tier. Defines currency, price, and billing interval (Monthly, Yearly, or Once for lifetime). Recurring plans auto-sync to PayPal Billing Plans.
Customer
An end-user identified by email (from Google identity) or a generated ID. Customers can have subscriptions across multiple plans.
Subscription
Links a customer to a plan/tier with a status lifecycle: Pending → Active → Suspended/Canceled/Expired. Also supports Trial status.
Trial
A time-limited free subscription (1–90 days) that grants access to a specific tier. Auto-starts on first use if configured. Converts to paid on upgrade.
Entity Relationships
User → Projects → Tiers → Plans
→ API Keys
→ Customers → Subscriptions → Payments
Agent-Based Development
Are you vibe-coding your app? Do you use code agents? Good for you!
Just give your agent PayVia's skill and/or let it use PayVia's MCP server, and you're all set!
Just tell your agent —
"Monetize my app / extension using PayVia!"
Skill
Download the skill file and place it in your project's skills folder. Your agent will automatically know how to integrate PayVia into your app.
Download SKILL.mdPlace at: your-project/.claude/skills/payvia/SKILL.md
PayVia MCP Server
Add the PayVia MCP server to your Claude Code configuration. It gives your agent tools to create projects, manage plans, configure PayPal, and handle subscribers — all through natural language.
Add to ~/.claude/settings.json:
{
"mcpServers": {
"payvia": {
"command": "npx",
"args": ["-y", "@payvia-sdk/mcp-server"],
"env": {
"PAYVIA_API_URL": "https://api.payvia.site"
}
}
}
}Authentication: The MCP server uses browser-based OAuth 2.0 with PKCE. On first use, it opens your browser for login. Tokens are stored at ~/.payvia/tokens.json and refreshed automatically.
SDK Reference
Configuration
Install the SDK or copy payvia.js into your project. Initialize with your project's API key from the PayVia dashboard.
import PayVia from './payvia.js';
const payvia = PayVia('pv_live_xxxxxxxxxxxxxxxx');The API key is found in your project settings under "API Keys". It starts with pv_.
Core Methods
getUser(options?)
(options?: { forceRefresh?: boolean }) => Promise<PayViaUser>
Returns the current user's payment status, tier, features, and trial info. Uses cache by default (7-day TTL with 30-day grace period). Pass { forceRefresh: true } to fetch from server.
const user = await payvia.getUser();
// {
// id: "user@gmail.com",
// email: "user@gmail.com",
// identitySource: "google", // "google" or "random"
// paid: true, // true if ACTIVE or TRIAL
// status: "ACTIVE", // "ACTIVE" | "TRIAL" | "INACTIVE"
// planIds: ["plan-uuid-1"],
// tier: { id: "tier-id", name: "Pro", level: 1, features: ["export_pdf", "api_access"] },
// features: ["export_pdf", "api_access"], // shortcut to tier.features
// isTrial: false,
// trialExpiresAt: null, // Date object if on trial
// daysRemaining: null, // number if on trial
// fromCache: false, // true if returned from cache
// checkedAt: 1709042400000, // when license was last checked
// ttl: 604800000, // cache TTL in ms (7 days)
// signature: "hmac-sig..." // anti-tamper HMAC signature
// }openPaymentPage(options)
(options: { mode?: 'pricing' | 'hosted' | 'direct', planId?: string, email?: string, successUrl?: string, cancelUrl?: string }) => Promise<{ mode, pricingUrl | checkoutUrl }>
Opens the payment page. Supports three checkout modes. If user has Google identity, email is auto-filled. Otherwise, email is required.
// Pricing mode (default) — shows all plans, user picks
await payvia.openPaymentPage({ mode: 'pricing', email: 'user@example.com' });
// Hosted mode — checkout for a specific plan
await payvia.openPaymentPage({ mode: 'hosted', planId: 'plan-uuid', email: 'user@example.com' });
// Direct mode — straight to PayPal (no PayVia UI)
await payvia.openPaymentPage({ mode: 'direct', planId: 'plan-uuid', email: 'user@example.com' });getPlans()
() => Promise<Plan[]>
Returns all available plans for your project.
const plans = await payvia.getPlans();
// [{ id: "plan-uuid", name: "Pro Monthly", price: 9.99, currency: "USD", interval: "Monthly", ... }]onPaid(callback)
(callback: (user: PayViaUser) => void) => () => void
Listens for payment status changes by polling every 5 seconds. Returns an unsubscribe function. Useful after opening a payment page.
const unsubscribe = payvia.onPaid((user) => {
console.log('Payment successful!', user.tier.name);
// Unlock features, show thank you, etc.
});
// Later: stop listening
unsubscribe();resetLicense()
() => Promise<{ message: string }>
Resets the current user's license (deletes all subscriptions). For testing/demo purposes only.
await payvia.resetLicense();
// { message: "Reset complete. Deleted 2 subscription(s)." }cancelSubscription(options?)
(options?: { planId?: string, reason?: string }) => Promise<{ success, message, canceledPlanId }>
Cancels the user's active subscription. Also cancels on PayPal if applicable.
await payvia.cancelSubscription({
planId: 'optional-plan-id', // cancel specific plan, or omit for first active
reason: 'User requested' // optional reason
});
// { success: true, message: "Subscription canceled successfully", canceledPlanId: "plan-uuid" }getIdentity()
() => Promise<{ id: string, email: string | null, source: 'google' | 'random' }>
Returns the current user's identity. Uses Google Chrome identity API first (email), falls back to a generated UUID.
const identity = await payvia.getIdentity();
// { id: "user@gmail.com", email: "user@gmail.com", source: "google" }
// or: { id: "pv_a1b2c3d4-...", email: null, source: "random" }needsEmailForPayment()
() => Promise<boolean>
Returns true if the user has no Google identity and must provide an email manually for payment.
if (await payvia.needsEmailForPayment()) {
// Show email input field before checkout
const email = prompt('Enter your email:');
await payvia.openPaymentPage({ email });
} else {
// Email auto-detected from Google identity
await payvia.openPaymentPage();
}Tier & Feature Methods
hasTierLevel(requiredLevel)
(requiredLevel: number) => Promise<boolean>
Check if the user's tier is at or above the required level. Tier levels: 0 = Free, 1 = Pro, 2 = Super (or custom).
// Check if user has Pro (level 1) or above
if (await payvia.hasTierLevel(1)) {
showProFeatures();
}hasFeature(featureName)
(featureName: string) => Promise<boolean>
Check if the user's tier includes a specific feature string.
if (await payvia.hasFeature('export_pdf')) {
enableExportButton();
}
if (await payvia.hasFeature('api_access')) {
showApiSection();
}getTier()
() => Promise<{ id, name, level, features } | null>
Returns the user's current tier info, or null if no tier (inactive user).
const tier = await payvia.getTier();
// { id: "tier-uuid", name: "Pro", level: 1, features: ["export_pdf", "api_access"] }
// or null if user has no active subscriptionTrial Methods
startTrial()
() => Promise<{ subscriptionId, status, planId, planName, trialExpiresAt, daysRemaining } | null>
Start a free trial for the current user. Idempotent — if user already has a trial or active subscription, returns existing info. Returns null if trials are not configured for the project.
const trial = await payvia.startTrial();
if (trial) {
console.log('Trial started!', trial.daysRemaining, 'days remaining');
// { subscriptionId: "sub-uuid", status: "TRIAL", planId: "plan-uuid",
// planName: "Pro", trialExpiresAt: Date, daysRemaining: 14 }
}getTrialStatus()
() => Promise<{ status, trialExpiresAt, daysRemaining, canConvert, planIds }>
Get the user's trial status.
const status = await payvia.getTrialStatus();
// { status: "TRIAL", trialExpiresAt: Date, daysRemaining: 12,
// canConvert: true, planIds: ["plan-uuid"] }
// status can be: "TRIAL" | "ACTIVE" | "EXPIRED" | "NONE"isFirstRun()
() => Promise<boolean>
Check if this is the first time the extension/app is being used. Uses chrome.storage.local or localStorage.
// Typical first-run flow: auto-start trial
const isFirst = await payvia.isFirstRun();
if (isFirst) {
await payvia.startTrial();
await payvia.markFirstRunDone();
}markFirstRunDone()
() => Promise<void>
Mark the first-run flag as complete so isFirstRun() returns false on subsequent calls.
await payvia.markFirstRunDone();Cache Methods
refreshLicenseCache()
() => Promise<void>
Refresh the license cache from server if expired. Designed for background service worker refresh on browser startup.
// In background.js (service worker)
chrome.runtime.onStartup.addListener(async () => {
const payvia = PayVia('pv_live_xxx');
await payvia.refreshLicenseCache();
});refresh()
() => Promise<PayViaUser>
Force refresh user status from server, bypassing all caches. Returns updated user object.
// Force a fresh server check
const user = await payvia.refresh();
console.log('Fresh status:', user.status);User Object
The user object returned by getUser() contains all license, tier, trial, and cache information.
{
// Identity
id: "user@gmail.com", // Customer ID (email or generated UUID)
email: "user@gmail.com", // Email if available, null otherwise
identitySource: "google", // "google" (Chrome identity) or "random" (fallback UUID)
// License status
paid: true, // true if status is ACTIVE or TRIAL
status: "ACTIVE", // "ACTIVE" | "TRIAL" | "INACTIVE"
planIds: ["plan-uuid-1"], // List of active plan IDs
// Tier info
tier: {
id: "tier-uuid",
name: "Pro",
level: 1, // 0 = Free, 1 = Pro, 2 = Super
features: ["export_pdf", "api_access", "priority_support"]
},
features: ["export_pdf", "api_access", "priority_support"], // shortcut to tier.features
// Trial info (only populated during trial)
isTrial: false,
trialExpiresAt: null, // Date object when trial expires
daysRemaining: null, // Days left in trial
// Cache info
fromCache: false, // true if result came from local cache
checkedAt: 1709042400000, // Unix timestamp (ms) of last server check
ttl: 604800000, // Cache TTL in ms (7 days, server-controlled)
signature: "base64-hmac-sha256" // Anti-tamper HMAC signature
}Integration Patterns
Chrome Extension (Manifest V3)
1. manifest.json
{
"manifest_version": 3,
"name": "My Extension",
"version": "1.0.0",
"permissions": ["storage", "identity", "identity.email"],
"background": {
"service_worker": "background.js"
},
"action": {
"default_popup": "popup.html"
}
}identity + identity.email enable auto-detection of the user's Google email. Without them, a random ID is used and email must be provided manually for checkout.
2. background.js — License cache refresh
import PayVia from './payvia.js';
const payvia = PayVia('pv_live_xxxxxxxx');
// Refresh license cache on browser startup
chrome.runtime.onStartup.addListener(async () => {
await payvia.refreshLicenseCache();
});
// Refresh on extension install/update
chrome.runtime.onInstalled.addListener(async () => {
await payvia.refreshLicenseCache();
});3. popup.js — Full integration
import PayVia from './payvia.js';
const payvia = PayVia('pv_live_xxxxxxxx');
async function init() {
// Auto-start trial on first run
const isFirst = await payvia.isFirstRun();
if (isFirst) {
await payvia.startTrial();
await payvia.markFirstRunDone();
}
// Get user status (uses cache, works offline)
const user = await payvia.getUser();
if (!user.paid) {
// Show upgrade prompt
showUpgradeUI();
return;
}
// Show trial banner if applicable
if (user.isTrial) {
showTrialBanner(user.daysRemaining);
}
// Feature gating — check by tier level
if (await payvia.hasTierLevel(1)) { // Pro or above
enableProFeatures();
}
// Feature gating — check by feature name
if (await payvia.hasFeature('export_pdf')) {
document.getElementById('export-btn').disabled = false;
}
}
async function handleUpgrade() {
// Check if we need to ask for email
if (await payvia.needsEmailForPayment()) {
const email = prompt('Enter your email for checkout:');
if (email) {
await payvia.openPaymentPage({ mode: 'pricing', email });
}
} else {
await payvia.openPaymentPage({ mode: 'pricing' });
}
// Listen for payment completion
payvia.onPaid((updatedUser) => {
showThankYou();
enableProFeatures();
});
}
init();SaaS Web App
app.js — Full integration
import PayVia from './payvia.js';
const payvia = PayVia('pv_live_xxxxxxxx');
// In SaaS context, Chrome identity is unavailable.
// The SDK falls back to a random UUID stored in localStorage.
// You must provide the user's email for checkout.
async function onLogin(userEmail) {
// Auto-start trial for new users
const isFirst = await payvia.isFirstRun();
if (isFirst) {
await payvia.startTrial();
await payvia.markFirstRunDone();
}
const user = await payvia.getUser();
if (user.paid) {
// Check tier access
const tier = await payvia.getTier();
console.log('User tier:', tier.name, 'Level:', tier.level);
// Gate features
if (await payvia.hasFeature('export_pdf')) {
showExportButton();
}
if (user.isTrial) {
showBanner(`Trial: ${user.daysRemaining} days remaining`);
}
} else {
// Show pricing — email is required in SaaS context
document.getElementById('upgrade-btn').onclick = () => {
payvia.openPaymentPage({ mode: 'pricing', email: userEmail });
};
}
}
onLogin('user@example.com');Key difference: In SaaS apps, there is no Chrome identity API. The SDK uses a random UUID (stored in localStorage) as the customer ID. You must pass the user's email explicitly to openPaymentPage().
REST API Reference
All endpoints use https://api.payvia.site as base URL. Authentication is via X-API-Key header.
License Endpoints
/api/v1/license/validateAuth: X-API-Key
Validate a customer's license status. Returns tier, features, trial info, and cache signature.
Request body:
{
"customerId": "user@gmail.com", // required
"email": "user@gmail.com" // optional, for anti-impersonation
}Response:
{
"status": "ACTIVE", // "ACTIVE" | "TRIAL" | "INACTIVE"
"planIds": ["plan-uuid"],
"tier": { "id": "tier-id", "name": "Pro", "level": 1, "features": ["export_pdf"] },
"isTrial": false,
"trialExpiresAt": null,
"daysRemaining": null,
"checkedAt": 1709042400000,
"ttl": 604800000,
"signature": "base64-hmac",
"version": "2.1.0"
}/api/v1/license/resetAuth: X-API-Key
Delete all subscriptions for a customer. For testing/demo only.
Request body:
{ "customerId": "user@gmail.com" }Response:
{ "message": "Reset complete. Deleted 2 subscription(s)." }/api/v1/license/cancelAuth: X-API-Key
Cancel a customer's subscription. Also cancels on PayPal if applicable.
Request body:
{
"customerId": "user@gmail.com", // required
"planId": "plan-uuid", // optional (cancels first active if omitted)
"reason": "User requested" // optional
}Response:
{
"success": true,
"message": "Subscription canceled successfully",
"canceledPlanId": "plan-uuid"
}/api/v1/license/trackAuth: X-API-Key
Track extension events (feature gates, upgrades, installs) for analytics.
Request body:
{
"customerId": "user@gmail.com",
"action": "FEATURE_GATE", // FEATURE_GATE | UPGRADE_CLICK | CHECKOUT_START | EXTENSION_INSTALL
"featureName": "export_pdf" // optional, for FEATURE_GATE
}Response:
204 No Content/api/v1/license/versionAuth: X-API-Key
Get the current API version.
Response:
{ "version": "2.1.0" }Trial Endpoints
/api/v1/trial/startAuth: X-API-Key
Start a trial for a new user. Idempotent — returns existing trial/subscription if one exists. Returns 409 if user already used their trial.
Request body:
{
"customerId": "user@gmail.com", // required
"email": "user@gmail.com" // optional
}Response:
{
"subscriptionId": "sub-uuid",
"status": "TRIAL",
"planId": "plan-uuid",
"planName": "Pro Trial",
"trialExpiresAt": "2026-04-05T10:00:00Z",
"daysRemaining": 14
}/api/v1/trial/statusAuth: X-API-Key
Get trial status for a customer.
Request body:
{ "customerId": "user@gmail.com" }Response:
{
"status": "TRIAL", // "TRIAL" | "ACTIVE" | "EXPIRED" | "NONE"
"trialExpiresAt": "2026-04-05T10:00:00Z",
"daysRemaining": 12,
"canConvert": true,
"planIds": ["plan-uuid"]
}Checkout Endpoints
/api/v1/checkout/tokenAuth: X-API-Key
Create a secure, temporary checkout token (10-minute TTL). Replaces API key in client-facing URLs.
Request body:
{
"customerId": "user@gmail.com",
"customerEmail": "user@gmail.com",
"mode": "pricing", // "pricing" | "checkout" | "external"
"planId": "plan-uuid" // required for "checkout" mode
}Response:
{
"token": "ct_xxxxxxxxxxxx",
"expiresAt": "2026-03-05T10:10:00Z"
}/api/v1/checkout/plansAuth: X-API-Key
Get all active plans for the pricing page. Includes tiers if configured.
Response:
{
"projectName": "My Extension",
"plans": [
{ "id": "plan-uuid", "name": "Pro Monthly", "price": 9.99, "currency": "USD", "interval": "Monthly" }
],
"tiers": [
{ "id": "tier-uuid", "name": "Pro", "level": 1, "features": ["export_pdf"], "plans": [...] }
]
}/api/v1/checkout-sessionAuth: X-API-Key
Create a PayPal checkout session. Routes to PayPal Orders API (one-time) or Subscriptions API (recurring).
Request body:
{
"planId": "plan-uuid", // required
"customerId": "user@gmail.com", // required
"customerEmail": "user@gmail.com",
"successUrl": "https://...",
"cancelUrl": "https://..."
}Response:
{
"checkoutUrl": "https://www.paypal.com/checkoutnow?token=...",
"sessionId": "subscription-id"
}Subscription States
Subscriptions follow a deterministic state machine. Only valid transitions are allowed.
Common Flows
New user with trial: Pending → Trial → Active (when they pay) or Expired (if trial ends)
Direct purchase: Pending → Active → Canceled (user cancels) or Expired (subscription ends)
Payment issue: Active → Suspended → Active (after retry) or Canceled
License Caching
The SDK caches license data locally for offline resilience. The cache has three layers: in-memory, persistent storage, and server.
TTL
7 days
Server-controlled via ttl field
Grace Period
30 days
Allows offline access after TTL expires
Signature
HMAC-SHA256
Per-project secret, anti-tamper verification
Cache Flow
getUser()checks in-memory cache → return if available- Checks persistent cache (chrome.storage.local / localStorage) → return if within TTL
- Fetches from server → saves to both caches
- On network error: uses cached data if within grace period (TTL + 30 days)
Storage
chrome.storage.local in Chrome Extensions, localStorage in web apps. Key: payvia_license_cache.
Cache Structure
{
"status": "ACTIVE",
"tier": { "id": "...", "name": "Pro", "level": 1, "features": ["..."] },
"planIds": ["plan-uuid"],
"isTrial": false,
"trialExpiresAt": null,
"daysRemaining": null,
"checkedAt": 1709042400000,
"ttl": 604800000,
"signature": "base64-hmac-sha256"
}Background Refresh (Recommended)
// background.js — refresh cache when browser starts
chrome.runtime.onStartup.addListener(async () => {
const payvia = PayVia('pv_live_xxx');
await payvia.refreshLicenseCache(); // Only fetches if cache expired
});
// Also refresh on extension install/update
chrome.runtime.onInstalled.addListener(async () => {
const payvia = PayVia('pv_live_xxx');
await payvia.refreshLicenseCache();
});Feature Gating
PayVia supports three patterns for gating features. Choose the one that fits your app.
1. By Tier Level
Use hasTierLevel() to check if the user's tier is at or above a required level. Higher levels include all features of lower levels.
// Tier levels: 0 = Free, 1 = Pro, 2 = Super
if (await payvia.hasTierLevel(1)) {
// Pro features — also accessible by Super (level 2)
enableProDashboard();
}
if (await payvia.hasTierLevel(2)) {
// Super-only features
enableAdminPanel();
}2. By Feature Name
Use hasFeature() for granular feature checks. Features are defined as strings in each tier's configuration.
// Check specific features defined in your tier configuration
if (await payvia.hasFeature('export_pdf')) {
showExportButton();
}
if (await payvia.hasFeature('api_access')) {
enableApiSection();
}
if (await payvia.hasFeature('custom_selectors')) {
showSelectorEditor();
}3. By Plan ID (Legacy)
For backward compatibility, you can check specific plan IDs directly.
const user = await payvia.getUser();
if (user.planIds.includes('pro-monthly-plan-uuid')) {
// User has the Pro Monthly plan
}
// Note: prefer tier/feature-based checks over plan IDs
// Plan IDs are implementation details that may changeExample Tier Configuration
{
"tiers": [
{
"name": "Free",
"level": 0,
"features": ["basic_rtl", "auto_detect"],
"isFree": true
},
{
"name": "Pro",
"level": 1,
"features": ["basic_rtl", "auto_detect", "export_pdf", "api_access"],
"isFree": false,
"plans": [
{ "name": "Monthly", "price": 9.99, "interval": "Monthly" },
{ "name": "Yearly", "price": 79.99, "interval": "Yearly" },
{ "name": "Lifetime", "price": 199, "interval": "Once" }
]
},
{
"name": "Super",
"level": 2,
"features": ["basic_rtl", "auto_detect", "export_pdf", "api_access", "custom_selectors", "admin_panel"],
"isFree": false
}
]
}Need Help?
Can't find what you're looking for? We're here to help.