Tenant Onboarding & Billing Integration Guide
How a new tenant connects their platform to our multi-tenant billing engine — Stripe setup, catalog configuration, checkout integration, and webhooks — so they can sell subscriptions and one-time products to their own customers.
Prepared from the current backend implementation (Node.js / Express / MongoDB, per-tenant database architecture). Document version 1.0.
1. Architecture at a Glance
This platform is a database-per-tenant billing engine: one Node.js/Express backend serves every tenant, but each tenant's data (catalog, orders, invoices, customers, payment gateway credentials) lives in its own MongoDB database. A request is routed to the correct tenant database by resolving a tenant from the incoming request, then binding that tenant's DB connection for the rest of the request lifecycle.
- Public storefront calls (
/public-api/*) are resolved by the request'sOrigin/Referer/Hostheader, matched against the domain you registered with us. - Your own staff/admin dashboard calls (
/admin/*) are resolved by a JWT (cookie/bearer) or anx-tenant-idheader. - Your end-customers' self-service calls (
/client/*) are resolved the same way, then further scoped to the logged-in customer's own records.
Important: tenant/database provisioning itself is not exposed through a public signup API today — it's a one-time setup our platform team performs (Section 4). Everything after that (catalog, checkout, self-service, webhooks) is what this guide walks you through.
Stripe model: bring your own account (not Stripe Connect)
Each tenant uses their own standalone Stripe account — you generate your own Stripe secret key and publishable key and hand them to us to store (encrypted) against your tenant. This is not Stripe Connect: we never call Stripe with on_behalf_of, application fees, or connected-account IDs. From Stripe's point of view, charges simply happen on your account, using your keys, initiated by our backend on your behalf.
| Backend | Node.js / Express, MongoDB (Mongoose) |
|---|---|
| Tenant isolation | One MongoDB database per tenant |
| Payment processor | Stripe — one Stripe account per tenant (your own account, your own keys) |
| Public integration surface | /public-api/* (your storefront) and /client/* (your customers' self-service portal) |
| Your admin surface | /admin/* (catalog, orders, settings — your staff dashboard) |
| Hosted checkout UI ("Payment App") | A separate React/Vite single-page app, deployed once and shared by every tenant — see Section 8.1 |
| Inbound Stripe webhook | POST /api/stripe/webhook (single shared endpoint, see Section 12) |
| Outbound webhooks to you | Configurable per tenant, see Section 11 |
There are three moving parts to this platform, and this guide covers how your integration touches each:
- The billing backend — the API described throughout this guide.
- Your own site — where you'll wire up the Admin API (catalog) and Client Portal API (customer self-service).
- The Payment App — a pre-built, Stripe-Elements-powered checkout page we host. You don't need to build your own payment form; you link/redirect your customers to it (Section 8.1). It is a separate codebase/deployment from the billing backend.
2. Key Concepts & Glossary
| Term | What it means here |
|---|---|
| Tenant | Your organization on the platform. Has its own MongoDB database, its own Stripe account, its own catalog and customers. |
| Project / Product Origin | Inside your tenant, a "project" (stored as a projectgeneralsettings document) represents one sellable storefront/product line — it holds your registered origin domain, enabled payment gateway(s), currency settings, and outbound webhook configuration. Most tenants have one project; you can have more than one if you run multiple storefronts. |
| Gateway | A payment method configuration record (paymentgateways collection) — for Stripe, this stores your encrypted secret key and publishable key. A Plan references the gateway it should charge through. |
| Product | A catalog entry describing what you're selling and its pricing shape (free / one-time / recurring), independent of billing mechanics. |
| Plan | The sellable, billable entity your customers actually purchase — has price(s) per currency, a billing interval, trial settings, feature entitlements, and optional add-ons. Plans and Products are first-party records only — we do not create matching Product/Price objects in Stripe. Stripe only ever sees a raw charge amount at the moment of payment. |
| Order | One purchase transaction — a plan purchase, an add-on purchase, or a one-time product purchase. Drives recurring billing state for subscriptions. |
| Subscription | The active/ongoing state of a recurring Order (status, next billing date, feature usage). This is our own subscription object — there is no corresponding Stripe Subscription object; renewals are charged by us re-using the saved payment method (Section 12). |
| Invoice | Our own PDF-generated invoice/receipt record tied to an Order. Not a Stripe Invoice object. |
| Transaction | A record of one payment attempt/result — success, failure, refund, chargeback — carrying the Stripe PaymentIntent/Charge IDs for reconciliation. |
3. Onboarding Checklist (Quick Start)
End-to-end sequence from "we've agreed to onboard you" to "your customers can pay you."
| # | Step | Owner |
|---|---|---|
| 1 | Platform team creates your tenant database and base project record | Us |
| 2 | You generate a Stripe secret key + publishable key on your own Stripe account and send them to us securely | You |
| 3 | We store your Stripe keys (encrypted) as a Gateway record and link it to your project | Us |
| 4 | You tell us your storefront domain(s) and base currency | You |
| 5 | We register your domain as your project's origin so /public-api/* resolves to your tenant | Us |
| 6 | You (or we, on request) create your Products/Plans via the Admin API — pricing, currencies, trials, entitlements | You/Us |
| 7 | You link your customers to the hosted Payment App for checkout (or build your own UI against the Purchase API) | You |
| 8 | You wire your customer account pages to the Client Portal API (plan changes, cancellation, invoices, saved cards) | You |
| 9 | You give us a webhook URL (and choose an auth mode) to receive payment/subscription/invoice events | You |
| 10 | Go live — test a real card in Stripe test mode end-to-end before flipping to live keys | You |
4. Step 1 — Platform Provisioning
This step is performed by our platform/ops team — there is currently no public self-service signup endpoint for creating a new tenant. We will:
- Create a
TenantRegistryentry (your tenant's unique name, display name, and the connection details for your dedicated database). - Provision your dedicated MongoDB database.
- Create your project (
projectgeneralsettings) document — the record that will hold your storefront origin, currency, enabled gateway(s), and webhook config from later steps.
5. Step 2 — Connect Your Stripe Account
We charge your customers using your Stripe account. You do not need to give us access to your Stripe dashboard — only two API keys.
| Stripe secret key | sk_test_... for testing, sk_live_... to go live |
|---|---|
| Stripe publishable key | pk_test_... / pk_live_... — used by your checkout page to mount Stripe.js/Elements and tokenize cards |
We store both keys encrypted in a paymentgateways record (provider: "stripe") inside your tenant database and link it to your project so Plans and Orders resolve back to it automatically at charge time.
on_behalf_of, no application fee. The Stripe API calls made on your behalf are:
stripe.customers.create— one Customer per payer, first purchasestripe.paymentMethods.attach— attaches the card token from your checkout pagestripe.paymentIntents.create(confirm: true) — the actual chargestripe.paymentIntents.confirm— resumes a charge after 3D Secure (Step 6)stripe.paymentIntents.create(off_session: true) — automatic recurring renewal charges (Section 13)stripe.refunds.create/stripe.charges.*— refunds issued from your admin dashboard
stripe.products.*, stripe.prices.*, stripe.checkout.sessions.*, stripe.subscriptions.*, or stripe.coupons.* — your catalog and subscription state live only in our database.
Multiple payment methods per project: a project can have more than one active Gateway (e.g. Stripe plus an offline "Bank Transfer" gateway). When a Plan doesn't pin a specific gateway, the resolver prefers, in order: an active Stripe gateway → any active online gateway → any active gateway with a key.
6. Step 3 — Register Your Storefront Origin & Currency
Your public checkout pages (/public-api/*) are resolved to your tenant purely by request origin — no API key is sent on these calls. Tell us the exact domain(s) your storefront runs on (e.g. https://shop.yourcompany.com) so we can register it as your project's origin.
/public-api/* trusts the Origin / Referer / Host header rather than a bearer token, it is designed to be called from a browser on your registered domain. If you plan to call it server-to-server (not from a browser), you must set a matching Origin / Host header yourself — treat this as origin-based routing, not as an authentication mechanism, and don't rely on it to keep data private beyond routing.
Bootstrapping the tenant ID client-side
GET /public-api/tenant/fetch-tenant?origin=https://shop.yourcompany.com
200 OK
{
"_id": "6512f...e91",
"tenant_unique_name": "yourcompany"
}
Also confirm your supported currency list:
GET /public-api/settings/global-settings/general-settings/currency-list?projectId={projectId}
7. Step 4 — Build Your Product & Plan Catalog
Catalog management happens on your Admin API (/admin/*), authenticated as your own staff user (JWT). This is a plain database write on our side — no Stripe Product/Price is created; Stripe only sees a bare amount + currency when a customer actually pays.
Core fields you'll set on a Plan
| Field | Meaning |
|---|---|
billingType | 1 = one-time, 2 = recurring |
billingInterval | Recurring cadence in months: 1, 3, 6, 12, 24, 36 |
currency / availableCurrencies / prices | Base currency plus a per-currency price book |
trialPeriod / trialDays | Free trial before first charge |
features | Reference to a Feature Category — the entitlements (usage limits, on/off flags) a subscriber gets |
addons[] | Optional extra purchasable items alongside the plan |
gateway | Which Gateway (Section 5) this plan charges through |
slug | The public identifier your storefront/API calls use to reference this plan |
Where catalog is managed
| Method | Path | Purpose |
|---|---|---|
| POST | /admin/product | Create a Product |
| POST | /admin/plans | Create a Plan (pricing, cycles, features, addons, gateway) |
| GET | /public-api/plan?origin= | List active plans for your storefront (public, read-only) |
| GET | /public-api/plan/:planSlug/features | Feature list for one plan (public) |
| GET | /public-api/plan/:planSlug/quote?origin=¤cy=&cycleId= | Display-only price quote for a plan in a given currency/cycle. The real amount is re-derived server-side at checkout — never trust a client-computed price. |
Discounts. Coupons are entirely first-party (tenant-scoped rules, redemption ledger, usage caps) — never synced to Stripe as coupons or promotion codes. The discount is computed in our database and only the final, already-discounted amount is sent to Stripe as the PaymentIntent amount.
8. Step 5 — Integrate Checkout on Your Site
There are three integration paths, in order of how much frontend work they require. Most tenants should just use the hosted Payment App (8.1) — it's a finished checkout page we already built and host; you generate a link to it, and Stripe Elements/3D Secure/receipt are all handled for you.
8.1 — Recommended: Redirect to the Hosted Payment App (zero frontend code)
- Create a payment link for a plan from your admin dashboard (or via API):
POST /public-api/payment-links { "sourceType": 3, "sourceReferenceSlug": "pro-monthly" } - The backend builds a checkout URL against the Payment App's shared domain, e.g.
https://pay.ourdomain.com/pay?payload=<encrypted>— the encrypted payload already carries the plan, price, currency, yourproductOrigin, and your Stripe publishable key, so the Payment App needs no separate login or tenant lookup to render correctly. - Link, email, or button your customer straight to that URL (or open it in an iframe/new tab from your site) — no Stripe.js integration required on your side.
- The Payment App collects the card with Stripe Elements, calls
/public-api/purchaseand/public-api/purchase/confirmitself (Section 9), and on success navigates to its own/payment-success?summaryRef=...receipt page.
pay.yourcompany.com) or custom branding on the checkout page, ask our platform team — today it's one shared, unbranded deployment used by every tenant.
8.2 — Custom-branded checkout: Payment Links + your own UI
If you want full control over the checkout page's look and feel (rather than the shared Payment App), you can build your own form and still use the same publishable-key lookup:
- Create the payment link as above, then validate the slug from your own checkout page to get plan details and the Stripe publishable key:
GET /public-api/payment-links/validate/:slug 200 OK { "plan": { "...": "..." }, "provider": "stripe", "publicKey": "pk_test_...", "gatewayId": "..." } - Collect the card with your own Stripe Elements integration client-side (card number never touches our servers), then submit the resulting PaymentMethod/token to the Purchase API below.
8.3 — Direct: Purchase API (fully headless / custom checkout UI)
A single unified endpoint handles orders, standalone invoices, and plan purchases, distinguished by sourceType:
| sourceType | Meaning | Effect on success |
|---|---|---|
| 1 | Order | Invoice + Transaction + card charge |
| 2 | Invoice | Transaction + card charge against an existing invoice |
| 3 | Plan | Order + Invoice + Subscription + Transaction (this is what powers a new subscription) |
POST /public-api/purchase
{
"productOrigin": "6512f...e91",
"customerInfo": { "email": "[email protected]", "name": "Jane Doe", "mobile": "+1..." },
"paymentMethodId": "pm_1P...",
"planInfo": {
"paymentSourceDetails": {
"sourceType": 3,
"sourceReferenceSlug": "pro-monthly"
},
"metadata": { "your_internal_customer_id": "cus_local_123" }
}
}
// success
200 OK
{ "status": "success", "payment_intent": { "...": "..." }, "receipt": { "summaryRef": "..." } }
// card requires authentication (3D Secure) — see Step 6
200 OK
{ "status": "requires_action", "clientSecret": "pi_..._secret_...", "receipt": { "summaryRef": "..." } }
// card declined
402 { "status": "payment_failed", "message": "..." }
/public-api/purchase directly (skipping Payment Links), you'll need to obtain your own publishable key out of band (you already have it — it's the one you generated in Step 2) rather than expecting our API to hand it back to you.
Pre-flight helpers
| Method | Path | Purpose |
|---|---|---|
| GET | /public-api/purchase/stripe-minimum?currency=&amount= | Validate the amount clears Stripe's minimum chargeable amount for that currency before you show a "Pay" button. |
| GET | /public-api/purchase/summary/:slug | Post-payment summary/receipt data (slug = receipt.summaryRef from the purchase response) — good for a "thank you" page. |
| POST | /public-api/purchase/addon, /addon/confirm | Purchase an add-on alongside/after a plan. |
POST /public-api/purchase/subscription/update and the underlying subscription.controller.js handler are non-functional placeholders (hardcoded plan/amount) — not wired to real traffic. For changing an existing subscriber's plan, use the Client Portal endpoint in Section 10 (/client/plan/:slug/change-plan) instead.
9. Step 6 — Handle 3D Secure / SCA
When Stripe requires additional authentication, the Purchase API returns status: "requires_action" with a PaymentIntent clientSecret. Your checkout page must:
- Call
stripe.confirmCardPayment(clientSecret)client-side (Stripe.js) to complete the 3DS challenge. - Then call our confirm endpoint to finalize the order on our side:
POST /public-api/purchase/confirm { "paymentIntentId": "pi_...", "receiptRef": "..." }
Only after this confirm step succeeds are the Order/Invoice/Subscription records created — a PaymentIntent stuck in requires_action that the customer abandons produces no order.
10. Step 7 — Give Customers Self-Service (Client Portal API)
Once a customer has an account with you, everything under /client/* is scoped to them — authenticated via a JWT (cookie <projectToken>_auth_token, an Authorization / auth-token header, or a ?token= query param).
| Method | Path | Purpose |
|---|---|---|
| GET | /client/plan | List plans available to this customer |
| GET | /client/plan/subscription | The customer's current subscription |
| GET | /client/plan/subscription/dashboard | Usage/entitlement dashboard data |
| GET | /client/plan/:slug/change-plan/options | Valid upgrade/downgrade targets from the current plan |
| POST | /client/plan/:slug/change-plan | Change plan (handles proration) |
| POST | /client/plan/:slug/change-plan/apply-zero | Apply a plan change that costs $0 (e.g. downgrade covered by credit) |
| POST | /client/plan/:slug/cancel | Cancel the subscription |
| GET | /client/invoices, /client/invoice/:id | List / fetch invoices |
| GET | /client/invoice/download-pdf, /download-receipt | Download invoice/receipt PDF |
| GET / POST | /client/card | List / add a saved card |
| PATCH | /client/card/:id/primary | Make a card the default payment method |
| DELETE | /client/card/:id | Remove a saved card |
| GET | /client/order, /client/transaction | Order / payment history |
| PATCH | /client/profile/preferred-currency | Update the customer's display/billing currency preference |
11. Step 8 — Receive Events From Us (Outbound Webhooks)
Tell us an HTTPS endpoint on your side and we'll POST you an event payload whenever something billing-relevant happens. Delivery is fire-and-forget — a failure on your end never blocks or rolls back the purchase.
Events we can send
| Event | Fired when |
|---|---|
payment.succeeded | A charge (order, invoice, or plan purchase) succeeds |
payment.failed | A charge attempt fails |
refund.succeeded | A refund is issued |
subscription.activated | A new subscription becomes active |
subscription.cancelled | A subscription is cancelled |
invoice.created | An invoice is generated |
invoice.paid | An invoice is marked paid |
order.created | A new order is recorded |
Choosing how we authenticate to you
| Mode | What you receive |
|---|---|
shared_secret (default) | A header (default name x-webhook-secret, or a custom header name you specify) carrying a secret you gave us — check it matches on every request. |
bearer | Authorization: Bearer <your secret> |
no_auth | No auth header at all — only use this on an endpoint you fully trust to be unguessable/internal. |
hmac_sha256 signing mode is reserved in the config schema but not implemented yet — don't select it.12. Step 9 — Our Inbound Stripe Webhook
Separately from Section 11, our backend itself listens for events coming from Stripe, at a single fixed endpoint: POST /api/stripe/webhook (verified via Stripe-Signature header).
What it actually handles today
| Stripe event | What we do with it |
|---|---|
charge.dispute.created | Flag the matching Transaction as disputed, hold for manual review, email our admin |
charge.dispute.closed | Settle the dispute record; if you lost the dispute, mark the linked invoice REFUNDED |
charge.refunded | Auto-book a Refund record and reconcile ledgers (deduplicated by refund ID) |
Everyday purchase success/failure is not driven by a Stripe webhook at all — the Purchase API (Section 8) confirms the PaymentIntent synchronously in the same request and writes the Order/Invoice/Subscription immediately. The Stripe webhook exists specifically to catch asynchronous events (disputes, refunds initiated from your Stripe dashboard) that can't be observed synchronously.
13. Money Mechanics
Invoices & receipts
Invoices are our own PDF documents (EJS template rendered to PDF via a headless browser), not Stripe Invoice objects — there is no stripe.invoices.* call anywhere in the flow.
Transactions
Every payment attempt (success, failure, refund, chargeback, or a wallet credit/debit) is recorded as a Transaction, carrying the Stripe PaymentIntent ID, Charge ID, and Customer ID for reconciliation against your Stripe dashboard.
Wallet credit
Each customer has one internal wallet (balance / credit balance / hold balance, integer minor units). It's a pure internal ledger — crediting a wallet never calls Stripe. Wallet balance is drawn down automatically before a card is charged where applicable.
Coupons
Entirely first-party — rules, eligibility, usage caps, and redemption tracking live in our database. Stripe is only ever shown the final discounted amount, never a coupon/promotion code object.
Multi-currency
You can charge in a "presentment" currency different from your own base/settlement currency. We persist the exchange rate and its source at the time of charge, and re-verify the PaymentIntent's actual currency/amount server-side before marking anything paid — mismatches are rejected rather than silently accepted. Zero-decimal currencies (e.g. JPY, KRW) and three-decimal currencies (e.g. KWD, BHD, OMR) are handled with Stripe's minor-unit rules in mind; always check /public-api/purchase/stripe-minimum before charging a small amount in an unfamiliar currency.
Recurring billing (subscriptions)
paymentIntents.create with off_session: true) on each subscription's stored nextBillingDate, and advances that date on success. After repeated failed renewal attempts the subscription is suspended, and once retries are exhausted the underlying order is moved to a terminal cancelled state.
14. API Reference Index
Public Storefront API — /public-api/* (origin-resolved, no token)
| Method | Path | Purpose |
|---|---|---|
| GET | /public-api/tenant/fetch-tenant | Resolve tenant by origin |
| GET | /public-api/plan | List active plans |
| GET | /public-api/plan/:planSlug/features | Plan feature list |
| GET | /public-api/plan/:planSlug/quote | Display price quote |
| GET | /public-api/input-options/countries, /states | Address dropdown data |
| GET | /public-api/settings/.../currency-list | Supported currencies |
| POST | /public-api/payment-links | Create a hosted checkout link (plan only) |
| GET | /public-api/payment-links/validate/:slug | Validate link, get plan + publishable key |
| POST | /public-api/purchase | Create payment (order / invoice / plan) |
| POST | /public-api/purchase/confirm | Confirm after 3D Secure |
| GET | /public-api/purchase/summary/:slug | Post-payment summary |
| GET | /public-api/purchase/stripe-minimum | Pre-validate minimum chargeable amount |
| POST | /public-api/purchase/addon, /addon/confirm | Add-on purchase |
Client Portal API — /client/* (customer JWT)
See the full table in Section 10 above.
Payment App (hosted checkout SPA — separate deployment)
| Path | Purpose |
|---|---|
/pay?payload=<encrypted> | The checkout form itself — Stripe Elements, coupon entry, 3D Secure. The payload is generated by the backend (Section 8.1); do not construct it yourself. |
/payment-success?summaryRef=... | Post-payment receipt page, reached automatically after checkout completes. |
/privacy-policy | Static privacy policy page. |
Your Admin API — /admin/* (staff JWT / x-tenant-id)
| Method | Path | Purpose |
|---|---|---|
| POST | /admin/product | Create a Product |
| POST | /admin/plans | Create a Plan |
| GET | /admin/selectOptions/card/gateways | List configured payment gateways |
| GET | /admin/selectOptions/card/gateway-public-key | Fetch a gateway's publishable key (staff-only) |
| GET | /admin/settings/global-settings/project | Read your project settings (origin, gateways, webhooks) |
| GET | /admin/transactions, /admin/order, /admin/invoice | Back-office views of orders/invoices/transactions |
| POST | /admin/wallet/add | Add wallet credit to a customer |
| POST | /admin/coupon | Create a discount coupon |
Project settings (origin, gateway selection, outbound webhook config) are currently read-only via API — changes to these go through our platform team until a write endpoint is exposed.
15. Status Code Reference
Order status
| Value | Meaning |
|---|---|
| 1 | Pending |
| 2 | Active |
| 3 | Fraud |
| 4 | Cancelled |
| 5 | Incomplete |
| 6 | Complete |
Invoice status
| Value | Meaning |
|---|---|
| 1 | Unpaid |
| 2 | Paid |
| 3 | Cancelled |
| 4 | Overdue |
| 5 | Refunded |
| 6 | Partially paid |
| 7 | No amount due |
Transaction status
| Value | Meaning |
|---|---|
| 1 | Pending |
| 2 | Processing |
| 3 | Succeeded |
| 4 | Failed |
| 5 | Cancelled |
16. Known Limitations & Recommendations
| Area | Current state | Recommendation |
|---|---|---|
| Tenant provisioning | Manual/internal only, no signup API | Budget lead time with our platform team for new tenants |
| Stripe catalog sync | No Stripe Products/Prices are ever created | Treat Stripe purely as your processor/statement source, not your source of catalog truth — always use our Plan/Product records for pricing logic |
| Publishable key on direct Purchase API | Not returned by plan-listing or quote endpoints | Use the Payment Link flow, or keep your own copy of your publishable key client-side |
| Inbound Stripe webhook secret | One shared signing secret platform-wide | Coordinate with us before depending on dispute/refund webhook automation for your account |
/public-api/purchase/subscription/update | Non-functional placeholder | Use /client/plan/:slug/change-plan instead |
| Recurring billing | Cron-driven off-session recharge, not Stripe Subscriptions | Don't build tooling against Stripe's subscription/invoice objects for renewal state — poll our Order/Subscription status instead, or subscribe to our outbound webhooks (Section 11) |
| Payment App branding | One shared, unbranded deployment used by every tenant — no per-tenant theming or custom domain out of the box | Use it as-is for a fast launch; ask the platform team if you need white-label branding, or build your own checkout with Section 8.2/8.3 instead |
| Payment App origin/tenant auto-detection | Implemented in the code but currently disabled — the app relies entirely on the encrypted payload link for context, not on your domain | Always launch checkout via a link generated by the backend (Section 8.1) — don't expect the Payment App to resolve your tenant from a bare URL |
| Payment App scope | Checkout + receipt only — no pricing/plan-listing page and no subscription-management page | Build those on your own site using the Public API (Section 7) and Client Portal API (Section 10) |
This guide reflects the platform's current implementation as of its generation date. Endpoints and behavior may change — contact the platform team for the latest contract before building critical automation against undocumented response fields.
