Payments in LiteHQ flow through Stripe Connect. You — the operator — own the Connect account; we orchestrate the platform. Funds settle straight into your bank; Stripe is the only party handling card data; we never touch it.
This guide walks the end-to-end setup: why Connect (not Charges, not Direct), the onboarding flow, configuring webhook secrets, payout schedules, and the five most common Connect errors with resolutions.
Test mode is free. You can run the entire onboarding flow with Stripe test keys (sk_test_) without any real money moving. Switch to sk_live_when you're ready.
Section 1
Why Stripe Connect (not Charges)
Operator owns funds · LiteHQ is a payment processor on behalf of you
The simplest Stripe shape is Charges: customers pay a single Stripe account, you settle later. That works for one-operator setups but collapses for multi-tenant SaaS — and LiteHQ is multi-tenant by definition.
Three reasons we picked Connect
- Direct settlement. Tenant payments land in your bank — not ours. We never custody your money. This is a legal + tax simplification.
- Per-operator dispute handling.Chargebacks, refunds, and disputes go to your Connect dashboard. You don't need to wait for us to triage.
- Multi-currency without a per-currency platform account.Each operator picks their settlement currency at onboarding. We don't need to be in every market they are.
stripe_payment_method_id on the booking row is the only Stripe identifier we persist.Section 2
Onboarding flow walkthrough
~10 min · operator-attended · KYC + bank account + verification
Onboarding happens at signup time (or later if you skipped it). You're redirected to a Stripe-hosted page, fill in business + verification details, and come back to your LiteHQ dashboard with a connected account.
The full sequence
- In Settings → Payments, click Connect Stripe. We create a Stripe account in your jurisdiction and generate an
AccountLink. - The AccountLink redirects to Stripe's hosted onboarding. Fill in: business name, tax id (ABN / NZBN / EIN / VAT), bank account, owner ID verification.
- Stripe returns to our callback at
/v2/preview/onboarding(see step 7 of the onboarding flow). - We auto-create two webhook secrets and register both endpoints with Stripe. You see them surfaced in Settings → Webhooks.
- We mark your operator account
stripe_status='active'. From this point your public booking URL accepts paid bookings.
Section 3
Configuring webhook secret(s)
Two secrets · platform + connect · dual-secret verification
LiteHQ's Stripe webhook endpoint accepts events from both your platform account and your connected account. The endpoint verifies the signature against both secrets — whichever matches wins, neither matching fails the request.
The two secrets
STRIPE_WEBHOOK_SECRET. Platform-level events. Things likeaccount.updatedon your Connect account.STRIPE_WEBHOOK_SECRET_CONNECT. Connected-account events.charge.succeeded,payment_intent.succeeded, refunds.
// Both secrets are tried — the first that verifies wins.
// We never reveal which one matched to the caller (timing leak).
async function POST(req: Request): Promise<Response> {
const sig = req.headers.get('stripe-signature') ?? ''
const body = await req.text()
let event: Stripe.Event | null = null
for (const secret of [
process.env.STRIPE_WEBHOOK_SECRET!,
process.env.STRIPE_WEBHOOK_SECRET_CONNECT!,
]) {
try {
event = stripe.webhooks.constructEvent(body, sig, secret)
break
} catch {
// try the next secret
}
}
if (!event) return new Response('invalid signature', { status: 400 })
await processEvent(event)
return new Response('ok')
}cs_* and pi_*. The stripe_payment_intent_id column on bookings and invoices must hold a pi_* value — never a Checkout Session cs_* id. We have DB CHECK constraints that reject this (KARO-387, migration 20260514083734), but custom webhook code that copies session.id into the wrong column will fail loudly.Section 4
Payout schedule and bank linking
Daily/weekly/monthly · per-currency settlement · per-operator override
Stripe sweeps funds from your connected account to your linked bank on a schedule you choose. We default to daily for operators with steady volume and weekly for smaller operators (fewer wire fees on the receiving bank).
Setting the schedule
- In your Stripe dashboard → Settings → Payouts, choose Daily / Weekly / Monthly. Stripe applies a rolling 2-business-day hold for cards (configurable).
- Link a bank account. Stripe requires a 1-cent test deposit + confirmation for ACH accounts; instant for AU/NZ via PayTo/POLi.
- Multi-currency operators can add additional payout currencies — each currency gets its own bank.
Reconciling payouts in Xero
Stripe payouts arrive in your bank as a single batched line. We map each payout to its underlying invoices via the cron-xero-syncedge function, which writes a Bank Transaction in Xero and matches it against the payout's individual charges.
paidin Xero, our cron treats Xero as the source of truth for that invoice. Don't void a paid invoice in our DB without also voiding it in Xero — the cron will not push the void out (KARO-221). The expiry path calls XeroClient.voidInvoice() inline; any new void code paths need to do the same.Reference
Troubleshooting common Connect errors
Five errors that cover 80% of support tickets
These five errors are the most common Stripe Connect issues we see in support. Each includes the underlying cause and the exact resolution.
| Error | Cause | Resolution |
|---|---|---|
Connect account stuck in 'pending' | Stripe needs ID/ownership verification that hasn't been submitted. The onboarding link was abandoned mid-flow. | Re-issue the AccountLink in your dashboard (Settings → Stripe → Resume onboarding). The new link is valid for 7 days. |
Webhook secret missing on a connected account event | The connected-account webhook (STRIPE_WEBHOOK_SECRET_CONNECT) wasn't generated or was rotated without updating the env. | Regenerate from the Connect dashboard, copy the new whsec_…, update the env var, redeploy. Old + new both validate for 24h after rotation. |
Scheduled payout failed | Stripe couldn't fund the payout — usually because of a recent refund that hit a too-thin balance. | Top up via Stripe dashboard or wait for the next batch of charges to land. We retry the payout automatically next business day. |
Connect onboarding blocked on director details | Your jurisdiction requires director KYC and the form has more than one required field unfilled. | Open the Stripe-hosted re-verification link. We send the operator-owner an email when this fires — check spam. |
Refund larger than available balance | You tried to refund more than has settled on the connected account. | Wait for the funds to settle (T+2 for cards, T+1 for bank-funded payments) and re-try the refund. |
- Connect account stuck in 'pending'
Cause. Stripe needs ID/ownership verification that hasn't been submitted. The onboarding link was abandoned mid-flow.
Fix. Re-issue the AccountLink in your dashboard (Settings → Stripe → Resume onboarding). The new link is valid for 7 days.
- Webhook secret missing on a connected account event
Cause. The connected-account webhook (STRIPE_WEBHOOK_SECRET_CONNECT) wasn't generated or was rotated without updating the env.
Fix. Regenerate from the Connect dashboard, copy the new whsec_…, update the env var, redeploy. Old + new both validate for 24h after rotation.
- Scheduled payout failed
Cause. Stripe couldn't fund the payout — usually because of a recent refund that hit a too-thin balance.
Fix. Top up via Stripe dashboard or wait for the next batch of charges to land. We retry the payout automatically next business day.
- Connect onboarding blocked on director details
Cause. Your jurisdiction requires director KYC and the form has more than one required field unfilled.
Fix. Open the Stripe-hosted re-verification link. We send the operator-owner an email when this fires — check spam.
- Refund larger than available balance
Cause. You tried to refund more than has settled on the connected account.
Fix. Wait for the funds to settle (T+2 for cards, T+1 for bank-funded payments) and re-try the refund.
Next steps