Forge API
Every Forge app runs inside its own isolated runtime. The platform injects a $forge global that apps can call directly — no imports required. These methods bridge the running app back to the platform's UI for consent flows, marketplace operations, and integration management.
$forge is a global constant equivalent to window.$forge. Both forms work, but $forge is the recommended shorthand.
TypeScript types
The @ptkl/components package ships a dedicated forge-api sub-entry that provides the full $forge type declaration and named types for all method signatures.
Installation
Enabling $forge types globally
Add a single side-effect import in your app's entry point (e.g. main.ts or index.ts). This augments the global scope so both $forge and window.$forge are typed everywhere in your project — no per-file imports needed:
// main.ts
import '@ptkl/components/forge-api'
// `$forge` is now fully typed across the entire app:
const granted = await $forge.requestPermissions(['orders.create'])
const result = await $forge.requestMarketplaceInstall('my-integration', 'integration')
await $forge.openDeveloperIntegrations()
Importing individual types
When you need a type for a function parameter or return value, import it by name:
import type { ForgeAPI, MarketplaceInstallResult, OnboardingResult } from '@ptkl/components/forge-api'
// Use ForgeAPI when you want to reference the full interface
function initForge(api: ForgeAPI) {
api.requestPermissions(['orders.read'])
}
// Use MarketplaceInstallResult when handling the resolved value of requestMarketplaceInstall
async function ensureInstalled(): Promise<MarketplaceInstallResult> {
return $forge.requestMarketplaceInstall('smtp-integration', 'integration')
}
// Use OnboardingResult when handling the resolved value of openOnboarding
async function checkOnboarding(): Promise<OnboardingResult> {
return $forge.openOnboarding()
}
Available exports
| Export | Kind | Description |
|---|---|---|
| (side-effect) | import '@ptkl/components/forge-api' |
Declares $forge and window.$forge as globals with the ForgeAPI type. |
ForgeAPI |
interface |
Full shape of the $forge object. |
MarketplaceInstallResult |
interface |
Return type of $forge.requestMarketplaceInstall. |
OnboardingResult |
interface |
Return type of $forge.openOnboarding. |
$forge.requestPermissions
Prompt the current user to approve additional permissions at runtime. A modal consent dialog opens over the app, and the function resolves with the list of permissions the user actually granted.
When to use it
Use this when your app needs a permission that is not declared in runtime_permissions (or the user launching the app does not currently hold it). Common patterns:
- An action that is rarely needed and should not appear in the app's default token.
- An operation that requires explicit user acknowledgement before it runs.
Signature
| Parameter | Type | Required | Description |
|---|---|---|---|
permissions |
string[] |
✅ | Permission strings to request (e.g. 'orders.create'). |
reason |
string |
❌ | Human-readable explanation shown on the consent screen. |
Returns — a Promise<string[]> that resolves with the subset of requested permissions the user approved. Rejects if the user closes or dismisses the dialog.
Example
try {
const granted = await $forge.requestPermissions(
['orders.create'],
'Required to submit the order on your behalf.'
)
if (granted.includes('orders.create')) {
await placeOrder(cart)
} else {
showError('Permission was not granted.')
}
} catch {
// User closed the dialog
}
Multiple permissions
const granted = await $forge.requestPermissions(
['customers.read', 'customers.update', 'tags.write'],
'Needed to tag customers based on their purchase history.'
)
const missing = ['customers.read', 'customers.update', 'tags.write']
.filter(p => !granted.includes(p))
if (missing.length) {
console.warn('Not all permissions were granted:', missing)
}
Token update
Once the user approves, the platform mints a new token that includes the granted permissions and posts it back. The SDK picks it up automatically — you do not need to refresh the page or re-initialise anything. The next API call made through the SDK will carry the updated token.
$forge.requestMarketplaceInstall
Prompt the current user to install a marketplace item (integration, app, or template) from within your app. A modal install dialog opens and the function resolves once the user completes (or already has completed) the installation.
When to use it
Use this when your app depends on another marketplace item and wants to guide the user through installing it without leaving your app's context.
Signature
$forge.requestMarketplaceInstall(
itemId: string,
itemType: 'integration' | 'app' | 'template',
options?: { initiator?: string }
): Promise<{ id: string; type: string; already_installed: boolean }>
| Parameter | Type | Required | Description |
|---|---|---|---|
itemId |
string |
✅ | The marketplace item ID. |
itemType |
'integration' \| 'app' \| 'template' |
✅ | The item category. |
options.initiator |
string |
❌ | Override the initiating app name (defaults to the current app). |
Returns — a Promise that resolves with:
| Field | Type | Description |
|---|---|---|
id |
string |
The installed item's ID. |
type |
string |
The item type as returned by the platform. |
already_installed |
boolean |
true if the item was already installed before the user opened the dialog. |
Rejects if the user cancels or closes the dialog.
Example
try {
const result = await $forge.requestMarketplaceInstall(
'smtp-integration',
'integration'
)
if (result.already_installed) {
console.log('Integration was already installed — continuing.')
} else {
console.log('Integration installed successfully.')
}
await initSmtp()
} catch {
showError('The required integration was not installed.')
}
Checking already_installed
The already_installed flag lets you distinguish between a fresh install and a user who had the item installed before opening the dialog:
const { already_installed } = await $forge.requestMarketplaceInstall(
'payment-gateway',
'integration'
)
if (!already_installed) {
await showWelcomeTour()
}
$forge.openOnboarding
Open the platform onboarding flow in a modal overlay, directly from within your Forge app. Use this when your app is in waiting_for_onboarding status and needs the user to complete their workspace setup before the app can function.
When to use it
The platform calls this automatically when it detects that your app's status is waiting_for_onboarding. You can also call it manually when you want to re-surface the onboarding flow at a later point — for example, after an incomplete earlier session.
- The overlay loads the platform's
/onboardingpage inside an iframe (960 × 85 vh). - When the user completes all onboarding steps, the page signals completion and the overlay closes with
{ completed: true }. - If the user closes the overlay without finishing, it resolves with
{ completed: false }. - Calling
openOnboardingfrom within an iframe is a no-op — it resolves immediately with{ completed: false }to prevent nested overlays.
Signature
Returns — a Promise<OnboardingResult> that always resolves (never rejects):
| Field | Type | Description |
|---|---|---|
completed |
boolean |
true if the user finished all onboarding steps; false if they closed without finishing. |
Example
const { completed } = await $forge.openOnboarding()
if (completed) {
// Onboarding done — boot the app normally
await initApp()
} else {
// User dismissed without finishing — show a blocked state
showOnboardingRequired()
}
Preflight pattern
import '@ptkl/components/forge-api'
import type { OnboardingResult } from '@ptkl/components/forge-api'
async function bootstrap() {
const status = await fetchAppStatus()
if (status === 'waiting_for_onboarding') {
const result: OnboardingResult = await $forge.openOnboarding()
if (!result.completed) {
renderBlockedState()
return
}
}
renderApp()
}
Behaviour in ptkl forge dev
| Scenario | Behaviour |
|---|---|
--platform-url provided |
Opens a full modal overlay with the onboarding page loaded from <platform-url>/onboarding. |
--platform-url not provided |
Logs a warning and resolves immediately with { completed: false } — no overlay is shown. |
| Called from inside an iframe | Resolves immediately with { completed: false } — overlay is suppressed to prevent nested iframes. |
$forge.openDeveloperIntegrations
Open the platform's developer integrations management page in a modal overlay, directly from within your Forge app. The overlay closes when the user dismisses it, and the returned Promise resolves at that point.
When to use it
Use this when your app requires one or more integrations to be activated before it can operate, and you want to guide the user to the integrations screen without leaving the app context. Common pattern:
- A preflight check detects that required integrations are not yet active.
- Your app surfaces a blocked state and offers a direct "Open integrations" action.
- After the user activates the integrations and closes the overlay, your app re-runs its preflight check.
Signature
Returns — a Promise<void> that resolves when the user closes the integrations overlay. The function does not indicate which integrations were changed; your app should re-query its state after resolution.
Example
const missing = await checkRequiredIntegrations()
if (missing.length > 0) {
document.getElementById('open-integrations').addEventListener('click', async () => {
await $forge.openDeveloperIntegrations()
// User closed the overlay — re-run preflight
await runPreflight()
})
}
Behaviour in ptkl forge dev
| Scenario | Behaviour |
|---|---|
--platform-url provided |
Opens a full modal overlay with the integrations page loaded from <platform-url>/admin/developer/integrations. |
--platform-url not provided |
Logs a warning and resolves immediately — no overlay is shown. |
Availability
$forge is injected by the platform runtime. It is not available outside a Forge app context (e.g. in a standalone browser tab or during server-side rendering).
When developing locally with the Protokol Toolkit's ptkl forge dev command, all methods are shimmed automatically. Pass --platform-url <url> to enable the full overlay implementations.
Error handling
requestPermissions and requestMarketplaceInstall reject the returned Promise when the user dismisses the dialog. Always wrap calls in try/catch (or .catch()) to handle cancellation gracefully:
try {
await $forge.requestPermissions(['reports.export'])
} catch {
// User closed the dialog — no action needed
}
openDeveloperIntegrations and openOnboarding always resolve (never reject), so no error handling is required for them. Check the resolved value to determine the outcome:
Never make permissions a hard requirement
Your app should remain usable even if the user declines. Disable or hide the relevant action rather than blocking the entire app.