@payhook/extension
v0.11.0
Published
Payhook client for browser extensions — hook lifecycle, hosted checkout, and entitlements
Readme
@payhook/extension
Payhook's browser-extension SDK for hosted checkout, Stripe subscription management, trials, metered feature usage, and paid feature access without license keys.
Install
npm install @payhook/extensionCreate a publishable phk_live_… or phk_test_… key in the
Payhook dashboard. The prefix selects the
Stripe environment; bind the key to your published Chrome extension ID.
Recommended Manifest V3 architecture
Use one headless PayhookSession in the background service worker. Popups,
options pages, and content scripts should ask the background to perform payment
actions and read the persisted access snapshot instead of creating competing
SDK clients.
import { PayhookSession } from '@payhook/extension'
const payhook = new PayhookSession()
await payhook.config('phk_live_xxx', {
version: chrome.runtime.getManifest().version
})
payhook.on('change', (entitlement) => {
console.log('Paid access changed', entitlement.active)
})
const access = await payhook.getAccessState()
if (access.active) {
// Paid entitlement or active trial.
}The dashboard is authoritative for product release channels, trial policy,
branding, labels, and eligible plans. The SDK caches this configuration and
revalidates it with ETag.
Open hosted payment flows
Keep window creation in the background session:
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
let action
if (message.command === 'payhook:upgrade') {
action = payhook.openUnlock({ waitForClose: false })
} else if (message.command === 'payhook:manage') {
action = payhook.openManagePlan({ waitForClose: false })
}
if (!action) return
action
.then(sendResponse)
.catch((error) => sendResponse({ error: error.message }))
return true
})openUnlock() opens unlock.payhook.link with
the install hook, extension ID, version, environment, and optional email.
Closing the hosted popup triggers an entitlement refresh.
DOM-aware integration
For a simple persistent page with one SDK owner, use the DOM singleton:
import { payhook } from '@payhook/extension'
import '@payhook/extension/upgrade-button.css'
await payhook.config('phk_live_xxx')
payhook.mount(document.querySelector('#upgrade'))The singleton also automatically mounts buttons on
[data-payhook-unlock] elements.
Do not configure the singleton independently in multiple short-lived popup or
content-script contexts. Use PayhookSession when access is shared across
extension contexts.
Access state
const {
active,
entitlement,
trial,
config
} = await payhook.getAccessState()
payhook.isEntitled() // Paid access only.
payhook.isInTrial() // Active SDK trial only.
payhook.hasAccess() // Paid access or active trial.Usage-based fallback
Configure rolling limits for one or more features. Paid customers and active
trials bypass these counters; after the trial ends, consumeFeature() grants
access until either configured weekly or monthly limit is reached.
await payhook.config('phk_live_xxx', {
featureUsage: {
features: {
pro: { weekly: 100, monthly: 300 }
}
}
})
const result = await payhook.consumeFeature('pro')
if (!result.allowed) {
// Show the upgrade UI. result.usage identifies the period that was hit.
}
payhook.getFeatureUsage('pro')
payhook.canUseFeature('pro')
// Duration-based products can consume multiple units at once, then open a
// Payhook-hosted usage breakdown that also links to the unlock flow.
await payhook.consumeFeature('pro', { amount: elapsedWholeMinutes })
await payhook.openFeatureUsage({ feature: 'pro', unit: 'minute' })The counters are intentionally approximate and client-side. Events are stored
under the existing live/test Payhook storage slots. Product telemetry is also
buffered in that storage and sent only when usage exists: after 15 minutes,
after 15 accumulated units, when a cap is hit, or when explicitly flushed.
Failed deliveries stay queued with a five-minute retry cooldown. Each tracking
request carries an aggregate amount, so Payhook records actual usage units
instead of counting network requests.
Usage during an active trial or paid entitlement is buffered the same way for
product analytics, but it never consumes feature credits. Payhook segments the
three usage modes as feature_usage_used_<feature>,
feature_usage_trial_<feature>, and feature_usage_entitled_<feature>.
When Chrome exposes a signed-in profile id, the SDK hashes it locally with the extension id and includes a rolling recovery checkpoint in the same batched request. Payhook never receives the raw profile id or email. A fresh reinstall does one restore lookup and seeds an empty local ledger from that checkpoint; during normal use the local ledger remains authoritative and offline-capable.
Flush a short remaining session when your product becomes inactive or the extension service worker is about to suspend:
await payhook.flushFeatureUsageTelemetry({ force: true })The dashboard /v1/config response can provide the same featureUsage shape
when limits should be managed remotely.
For development, support, or a deliberate grant, resetCredits() additively
stamps a reset point for the usage history and restarts the configured trial:
await payhook.resetCredits()The SDK persists sanitized live/test entitlement snapshots for fast startup and
offline continuity. Stripe remains authoritative for subscription lifecycle;
Payhook selects the modern entitlement exactly once. Initialization loads the
snapshot before refreshing; network errors, 408, 429, and 5xx responses
keep the last good snapshot without advancing checkedAt. Explicit pull()
calls still reject, allowing the extension to show stale/offline state when
useful. Stored snapshots contain only entitlement fields and Stripe
product/price identifiers—never email or Checkout Session data.
Chrome manifest
{
"permissions": ["storage", "identity", "identity.email"],
"host_permissions": ["https://api.payhook.link/*"],
"externally_connectable": {
"matches": ["https://payhook.link/*", "https://*.payhook.link/*"]
}
}The native Chrome adapter reads and writes only the payhook sync-storage key,
reducing cross-feature storage races.
Storage and compatibility
- The payment hook ID is canonical and separated into live/test slots.
- The Customer Portal endpoint is derived as
${apiUrl}/v1/payment_hooks/${paymentHookId}/manage_plan. - Historical
managePlanUrlandbillingPortalUrlfields remain readable during compatibility windows but are not canonical state. - Trial configuration and clocks are persisted independently for live/test.
- Feature usage events and reset points are persisted independently for live/test. Old events remain readable after a reset for rollback/debugging.
- Recovery checkpoints are keyed by an extension-scoped SHA-256 profile hash; raw Chrome identity values are never persisted by Payhook.
- Migrations should copy older state additively and dual-write rollback shadows for a bounded window.
Main exports
PayhookSession— headless background owner.payhook— DOM-aware singleton.PayhookClient— lower-level client for custom lifecycle control.UpgradeButtonandmountUpgradeButton()— custom rendering.createChromeAdapters()— native key-scoped Chrome storage, identity, and window adapters.- Trial, hook-storage, and entitlement-storage helpers for explicit migrations.
- Feature-usage helpers for rolling caps and additive credit resets.
Documentation
License
ISC
