/iblai-vibe-monetization-onboard
Build the Stripe Connect Express onboarding surface — the first thing a
Platform owner sees on the Monetization tab. Until this surface flips
is_ready_for_payments to true, every paywall, price tier, and
checkout you wire up later will refuse to charge.
Common setup (brand, conventions, env files, verification): see docs/skill-setup.md.
Verify the API before you call it. Fetch the live OpenAPI schema at
{dm_url}/api/docs/schema/(browsable at{dm_url}/api/docs/;{dm_url}=https://api.$DOMAIN/dm,DOMAINfromiblai.env, defaultiblai.app) and confirm the URL path, method, request body, and response shape for every endpoint you reach for. The schema is the source of truth; the URLs in this skill exist for orientation and may drift between releases. See /iblai-vibe-monetization → references/schema-validation.md for the exact fetch routine.
Prerequisites
- Auth must be set up first (
/iblai-vibe-auth) — reuse the same token wiring .mcp.jsonconfigured with@iblai/mcp(and skills installed vianpx skills add iblai/vibe --all)iblai.envpopulated withPLATFORM,DOMAIN,TOKEN. If missing:curl -o iblai.env https://raw.githubusercontent.com/iblai/vibe/refs/heads/main/iblai.env- The Account page already exists (
/iblai-vibe-account) — the Stripe Connect status card lives inside theMonetizationprofile tab on that page, not on a standalone route - The caller holds the
IsPlatformAdminrole on the Platform. Connect endpoints are owner-only — non-owners get403and the status query comes back withis_owner: false, which causes the SDK to hide the card entirely - The Platform has
enable_monetization === trueon itscurrentTenantrecord. This flag is what surfaces theMonetizationtab insideAccountin the first place; if it is unset, the entire onboarding surface is invisible regardless of Connect state. See/iblai-vibe-monetizationfor the flag lifecycle.
What you'll build
A single Stripe Connect status card (the SDK component
StripeConnect from @iblai/iblai-js) with three states:
| State | Badge | Description | Action button |
|---|---|---|---|
| Ready | Configured (blue) |
"Stripe is connected and ready to accept payments." Optionally appends "Charges enabled." / "Payouts enabled." | Stripe Dashboard — opens the Stripe-hosted Express Dashboard in a new tab |
| Started but incomplete | Incomplete (yellow) |
"Stripe account created but onboarding is incomplete. Complete setup to start accepting payments." | Complete Setup — re-issues the onboarding link and redirects |
| Not started | Not Configured (grey) |
"Connect a Stripe account to enable monetization features." | Configure Stripe — kicks off onboarding |
The badge and button text come straight from the SDK — do not
re-skin them. The badge maps to the is_ready_for_payments /
has_account booleans on the status response, not to
onboarding_complete directly.
This card is auto-rendered inside MonetizationTab whenever the
status response carries is_owner: true. You do not mount it by
hand. The reason this skill exists is so that (a) you understand the
contract before you put the tab on a page, and (b) you can build a
custom Connect surface (for example, a setup wizard, an admin tool,
or a standalone /billing/setup route) that talks to the same five
endpoints when the default card does not fit.
Step 1: Validate the API schema
Before you write a single Connect call, walk through
references/connect-api.md — it
documents the five endpoints, request bodies, response fields,
permission classes, and the SDK hook map.
Then cross-check each path against the live schema as the schema-first
directive prescribes. The five Connect paths today (commerce tag) —
all hosted under the DM base ({dm_url}, e.g.
https://api.iblai.app/dm):
GET {dm_url}/api/service/platforms/{platform_key}/stripe/connect/status/POST {dm_url}/api/service/platforms/{platform_key}/stripe/connect/onboard/POST {dm_url}/api/service/platforms/{platform_key}/stripe/connect/onboard/refresh/GET {dm_url}/api/service/platforms/{platform_key}/stripe/connect/dashboard/DELETE {dm_url}/api/service/platforms/{platform_key}/stripe/connect/
All five require IsPlatformAdmin and run platform-owner validation
in the service layer on top of that. All require Authorization: Token <DM token> — the DM token, not the AXD token; the two are different
tokens issued by different services. Using the AXD token returns 401.
Step 2: Mount the status query and gate on is_ready_for_payments
Mount the status query and read is_ready_for_payments:
'use client';
import { useGetStripeConnectStatusQuery } from '@iblai/iblai-js/data-layer';
export function ConnectGate({ platformKey }: { platformKey: string }) {
const { data, isLoading, isError } = useGetStripeConnectStatusQuery(
{ platform_key: platformKey },
{ refetchOnMountOrArgChange: true },
);
if (isLoading) return <p>Checking Stripe…</p>;
if (isError) return <p>Stripe status unavailable.</p>;
if (!data?.is_owner) return null; // hide card for non-owners
if (!data.is_ready_for_payments) {
return <p>Finish Stripe onboarding before publishing paywalls.</p>;
}
return <p>Ready to accept payments.</p>;
}
is_ready_for_payments is the only field you should branch on for the
"can this Platform accept payments today?" decision. It composes
!is_disconnected && onboarding_complete && charges_enabled on the
backend — has_account: true flips long before payments work and
will mislead you if you check it directly.
{ refetchOnMountOrArgChange: true } matters here: after the Stripe
redirect lands the user back on your page, the cached status from
before onboarding is stale. The default SDK card sets this option for
the same reason.
Step 3: Trigger onboarding
Use the onboarding mutation. The hook lives at the same export path:
'use client';
import { useStartStripeConnectOnboardingMutation } from '@iblai/iblai-js/data-layer';
function ConfigureButton({ platformKey }: { platformKey: string }) {
const [startOnboarding, { isLoading }] =
useStartStripeConnectOnboardingMutation();
const handleClick = async () => {
const returnUrl = new URL(window.location.href);
returnUrl.searchParams.set('profileTab', 'monetization');
const url = returnUrl.toString();
const result = await startOnboarding({
platform_key: platformKey,
return_url: url,
refresh_url: url,
business_type: 'company', // SDK default
}).unwrap();
window.location.href = result.onboarding_url;
};
return (
<button disabled={isLoading}>
Configure Stripe
</button>
);
}
A few things to know:
- The SDK's bundled
StripeConnectcomponent hardcodesbusiness_type: 'company'today. If you need theindividualvariant (sole traders, freelancers selling their own agent), do not try to override the SDK card — build a custom onboarding-start screen against the same mutation and passbusiness_type: 'individual'. The schema enum accepts both values and defaults toindividualserver-side when the field is omitted. return_urlandrefresh_urlare both required by the request serializer. Using the same URL for both (as the SDK does) is fine.- Redirect with
window.location.href. Do not open the onboarding URL in an iframe — Stripe Connect blocks framing. - The mutation invalidates the
stripeConnectStatuscache tag, so any status query already mounted in your tree will refetch automatically once the user returns.
Step 4: Handle return + refresh
After the owner completes (or abandons) onboarding, Stripe redirects the browser back to the URL you supplied:
return_url— fires on completion. Your page should re-mount, the status query should refetch,is_ready_for_paymentsshould now be true.refresh_url— fires when the onboarding link expires (Stripe Connect links are short-lived) or when the owner exits the flow. Your page should detect this and re-issue an onboarding URL by calling the refresh endpoint:
// All Connect endpoints are mounted on the DM service, not the AXD edge.
// Compose the base from your env or from SERVICES.DM if the SDK is loaded.
const dmBase = `${apiBase}/dm`; // e.g. https://api.iblai.app/dm
const res = await fetch(
`${dmBase}/api/service/platforms/${platformKey}/stripe/connect/onboard/refresh/`,
{
method: 'POST',
headers: {
// DM token — not the AXD token. The SDK injects this automatically
// when you go through RTK Query; the direct fetch needs it spelled.
Authorization: `Token ${dmToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
return_url: window.location.href,
refresh_url: window.location.href,
business_type: 'company',
}),
},
);
const { onboarding_url } = await res.json();
window.location.href = onboarding_url;
In Next.js, your App Router page must not strip the query params
Stripe appends (?profileTab=monetization is what tells your tab
which subview to show). If you read searchParams server-side, pass
them through; never use router.replace('/account') without
preserving the query string on the return landing.
Step 5: Show the dashboard link
Once is_ready_for_payments is true, surface a button that drops the
owner into the Stripe-hosted Express Dashboard:
import { useLazyGetStripeConnectDashboardQuery } from '@iblai/iblai-js/data-layer';
function DashboardButton({ platformKey }: { platformKey: string }) {
const [getDashboard, { isFetching }] = useLazyGetStripeConnectDashboardQuery();
const open = async () => {
const { data } = await getDashboard({ platform_key: platformKey });
if (data?.dashboard_url) window.open(data.dashboard_url, '_blank');
};
return <button disabled={isFetching}>Stripe Dashboard</button>;
}
Use the lazy variant — the dashboard URL is one-shot and tied to the
requesting session, so you fetch on click rather than on mount.
useGetStripeConnectDashboardQuery works too if you want
auto-fetching, but it will issue a request every time the component
mounts whether the user clicks or not.
Step 6: Disconnect (optional, admin-only)
DELETE {dm_url}/api/service/platforms/{platform_key}/stripe/connect/ is
defined in the schema and implemented at StripeConnectDisconnectView
(line 155 of views/stripe_connect.py), but the SDK does not currently
expose a hook for it. If you need to surface a disconnect option in a
custom admin tool, call it directly:
// Compose the DM base — Stripe Connect lives on DM, not the AXD edge.
const dmBase = `${apiBase}/dm`;
await fetch(
`${dmBase}/api/service/platforms/${platformKey}/stripe/connect/`,
{ method: 'DELETE', headers: { Authorization: `Token ${dmToken}` } },
);
Response shape:
- 200 — disconnect succeeded; the
StripeConnectAccountrow was soft-deleted. - 404 — the Platform has no Stripe Connect account row to delete (nothing was ever onboarded, or it was already disconnected).
The endpoint soft-deletes the StripeConnectAccount row — paywalls
keep their configuration but cannot charge until the Platform
re-onboards. Confirm twice in the UI before calling it; this is
production-grade revenue infrastructure.
Verify
-
Monetizationtab appears inside theAccountpage only whencurrentTenant?.enable_monetization === true - Status card renders only when the status response has
is_owner: true - Before onboarding: badge reads
Not Configured, button readsConfigure Stripe - Clicking
Configure Striperedirects to a URL onconnect.stripe.com(not a same-origin error page) - After returning from Stripe, the status query refetches and
is_ready_for_paymentsistrue - Badge then reads
Configured, button readsStripe Dashboard - Clicking
Stripe Dashboardopens a Stripe-hosted URL in a new tab - If you exited mid-flow, hitting the
refresh_urlre-issues an onboarding link without prompting for any data twice - Non-owner Platform admins (
is_owner: false) see the tab but not the Connect card
Common mistakes
- Gating off
has_account: trueinstead ofis_ready_for_payments.has_accountflips the moment the Connect account row is created, which is before KYC, before charges are enabled, and before Stripe will accept a payment intent. Reading it as "Stripe works now" is the most common reason paywalls publish but checkout fails with a Stripe error. - Skipping the
is_ready_for_paymentscheck entirely and assuming any user on the Monetization tab can configure paywalls. The Monetization tab's wizard is gated against this flag — surfacing pricing UI on top of an unready Connect account just produces a broken checkout downstream. - Stripping query params on the return URL. Next.js router methods
like
router.replace('/account')drop?profileTab=monetization, which leaves the owner on the wrong sub-tab when they return. Either readsearchParamsand preserve them, or usewindow.location.hreffor the redirect so Stripe round-trips you to the exact URL you supplied. - Forgetting to pass
authURLto the<Account>page. The MonetizationTab needs the auth-redirect URL to chain the user through if they hit a re-auth wall during onboarding. See/iblai-vibe-accountfor the prop contract. - Trying to override
business_type: 'company'by patching the SDK card. The string is hardcoded inside the component; reach for a custom screen againstuseStartStripeConnectOnboardingMutationinstead, and passbusiness_type: 'individual'there. - Treating the dashboard URL as cacheable. It is one-shot. Re-call the endpoint each time the owner clicks the button.
MCP tools for further detail
get_hook_info("useGetStripeConnectStatusQuery")get_hook_info("useStartStripeConnectOnboardingMutation")get_hook_info("useGetStripeConnectDashboardQuery")get_hook_info("useLazyGetStripeConnectDashboardQuery")get_component_info("MonetizationTab")get_component_info("StripeConnect")(the auto-mounted child card)
Files in this skill's scope
Frontend SDK (iblai/ibl-web-frontend on main):
packages/web-containers/src/components/profile/monetization/stripe-connect.tsx— the auto-rendered status cardpackages/web-containers/src/components/profile/monetization/index.tsx—MonetizationTabthat mounts the card behindis_ownerpackages/data-layer/src/features/monetization/custom-api-slice.ts— the four Connect hooks live in// Flow 1: Stripe Connectpackages/data-layer/src/features/monetization/types.ts—StripeConnectStatusResponse,StripeConnectOnboardArgs,StripeConnectOnboardResponse,StripeConnectDashboardResponse
Backend (ibl-dm-pro repo, dl_iblai_services_app):
views/stripe_connect.py—StripeConnectOnboardView(line 35),StripeConnectStatusView(line 92),StripeConnectDashboardLinkView(line 119),StripeConnectDisconnectView(line 155)models/stripe_connect.py—StripeConnectAccountmodel, theis_ready_for_paymentsandcommission_percentpropertiesservices/stripe/connect.py—StripeConnectService.get_account_status(), the source ofis_owneron the response
Related skills
/iblai-vibe-monetization— family overview + the schema-validation routine/iblai-vibe-monetization-configure— what to build right after this: theMonetizationTabwizard for paywall + price CRUD (depends onis_ready_for_payments === true)/iblai-vibe-monetization-checkout— the buyer-sidePaywallModaland Stripe checkout that this onboarding enables/iblai-vibe-account— hosts theMonetizationTaband supplies theauthURLprop/iblai-vibe-auth— the token wiring every Connect call inherits/iblai-vibe-rbac—IsPlatformAdminand Platform-owner gating- BRAND.md — visual conventions for any custom Connect surface you build