PENTOOLS_HUB_SECRET secret on your sub-app (same value as on pentools.fi)notifyPenToolsHub helper function (see below)registerWithHub once to appear in Connected AppsnotifyPenToolsHub from every subscription/payment eventHub endpoint URL (confirmed working)
⚠️ Getting a 404? The function URL is correct and confirmed live. A 404 from the sub-app side usually means:
HUB_URL constant in the sub-app's function wasn't saved/redeployed after editingCreate this as a backend function called notifyPenToolsHub on your sub-app. Replace YOUR_APP_ID and Your App Name with your app's values.
import { createClientFromRequest } from 'npm:@base44/sdk@0.8.25';
const HUB_URL = "https://pentools.fi/functions/hubReceiveEvent";
const APP_ID = "YOUR_APP_ID"; // e.g. "equilibrium"
const APP_NAME = "Your App Name"; // e.g. "EquilibriuM"
/**
* Notify the PenTools hub of a subscription or plugin event.
* Call this from any Stripe webhook, subscription handler, etc.
*/
async function notifyPenToolsHub(payload) {
const secret = Deno.env.get("PENTOOLS_HUB_SECRET");
const res = await fetch(HUB_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ secret, app_id: APP_ID, app_name: APP_NAME, ...payload }),
});
return res.json();
}
// Export as a callable backend function too (optional)
Deno.serve(async (req) => {
const base44 = createClientFromRequest(req);
const user = await base44.auth.me();
if (!user) return Response.json({ error: "Unauthorized" }, { status: 401 });
const body = await req.json();
const result = await notifyPenToolsHub(body);
return Response.json(result);
});Create this as a backend function called registerWithHub. After creating it, test it once from the Base44 Functions panel — your app will then appear as active in Connected Apps.
import { createClientFromRequest } from 'npm:@base44/sdk@0.8.25';
const HUB_URL = "https://pentools.fi/functions/hubReceiveEvent";
const APP_ID = "YOUR_APP_ID"; // e.g. "equilibrium"
const APP_NAME = "Your App Name"; // e.g. "EquilibriuM"
/**
* One-time registration ping — run this once after setting PENTOOLS_HUB_SECRET.
* It registers this app in the Connected Apps dashboard on pentools.fi.
*/
Deno.serve(async (req) => {
const secret = Deno.env.get("PENTOOLS_HUB_SECRET");
const res = await fetch(HUB_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
secret,
app_id: APP_ID,
app_name: APP_NAME,
event_type: "subscription_created",
user_email: "system@pentools.fi",
plan: "connected",
status: "active",
}),
});
const data = await res.json();
return Response.json({ message: "Registration ping sent", response: data });
});Plans are managed centrally on pentools.fi via the Plans Manager. Sub-apps can fetch their active plans at runtime using this function. The endpoint is getAppPlans on the pentools.fi hub.
Hub endpoint
https://pentools.fi/functions/getAppPlans
Returns: { plans: [...], addons: [...] } — plans (subscription tiers) and addons (plugin type plans), both sorted by sort_order. Each object includes name, price, billing_period, seats, admin_seats, features, tags, cta_url, is_highlighted, and more.
// Backend function: fetchPlansFromHub.js
// Fetches the centrally managed pricing plans for this app from pentools.fi.
// Call this from your pricing page or cache the result.
const GET_PLANS_URL = "https://pentools.fi/functions/getAppPlans";
const APP_ID = "YOUR_APP_ID"; // e.g. "equilibrium"
Deno.serve(async (req) => {
const secret = Deno.env.get("PENTOOLS_HUB_SECRET");
const res = await fetch(GET_PLANS_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ secret, app_id: APP_ID }),
});
const data = await res.json();
// data.plans = sorted array of active AppPlan records
return Response.json(data);
});