Developer Docs

Integration guides for connecting sub-apps to the PenTools hub

How it works

  1. Set the PENTOOLS_HUB_SECRET secret on your sub-app (same value as on pentools.fi)
  2. Create the notifyPenToolsHub helper function (see below)
  3. Run registerWithHub once to appear in Connected Apps
  4. Call notifyPenToolsHub from every subscription/payment event
📌

Hub endpoint URL (confirmed working)

https://api.base44.com/api/apps/69fafb7e0029f6e475055ed2/functions/hubReceiveEvent

⚠️ Getting a 404? The function URL is correct and confirmed live. A 404 from the sub-app side usually means:

  • The URL was hardcoded with the sub-app's own App ID instead of the pentools.fi App ID above
  • A typo or extra slash in the URL (copy it exactly as shown)
  • The HUB_URL constant in the sub-app's function wasn't saved/redeployed after editing

Create 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);
});