Billing Architecture
Vidivo’s billing system is the core differentiator of the platform. It uses a time-window hold/capture model built on Stripe PaymentIntents with manual capture, ensuring guests are never charged for time they do not use while guaranteeing hosts are paid for the time they provide.
Platform Payment Model
Section titled “Platform Payment Model”Vidivo operates a platform-managed payment model:
- The platform collects all payments directly from guests
- Hosts add bank accounts via Stripe Custom connected accounts
- Hosts request payouts, which the platform transfers via Stripe Payouts API
- A 7% platform fee is deducted from each captured payment
This model was chosen over Stripe Connect Express for greater control over the payout process and to simplify the host onboarding experience.
Time-Window Hold/Capture
Section titled “Time-Window Hold/Capture”How Windows Work
Section titled “How Windows Work”Each call is divided into billing windows of a configurable size (set by the host, typically 5—30 minutes). Before each window begins, a Stripe PaymentIntent hold is placed on the guest’s card. At the end of each window, the hold is captured (charged permanently).
Time: 0 10 20 30 min | | | | v v v v Holds: [--W1----][--W2----][--W3---] ^ ^ W1 captured W2 captured
At call end (e.g. 24 min): +-- W3 partial: 4 min x rate = captured +-- Remainder of W3 hold: releasedStep-by-Step Flow
Section titled “Step-by-Step Flow”-
Guest submits payment: A Stripe PaymentMethod is created via Stripe Elements on the client. Card data never touches Vidivo servers.
-
Window 1 hold: The billing service creates a PaymentIntent with
capture_method: manualforwindow_size x rate_per_minute. This places an authorization hold on the guest’s card. -
Pre-emptive next hold (at
window_size - 60s): Before the current window ends, a new PaymentIntent hold is placed for the next window. This ensures continuous coverage. -
Window capture (at
window_size): The current window’s PaymentIntent is captured. The full window amount is charged. -
Partial window capture (at call end): If the call ends mid-window, only the elapsed time is charged:
ceil(elapsed_minutes_in_window) x rate_per_minute. The remainder of the hold is released. -
Hold release: Any future window holds that were not captured are canceled, releasing the authorization immediately.
Why This Design?
Section titled “Why This Design?”| Concern | Solution |
|---|---|
| Guest runs out of funds mid-call | Pre-authorization detects insufficient funds before the call starts |
| Guest disputes charges for unused time | Only actual usage is captured; holds are released |
| Host is not paid for time | Captures happen at window boundaries, not at call end |
| Network failure during call | Billing timers run server-side (Redis), independent of client connections |
| Duplicate charges | Stripe Idempotency Keys on all PaymentIntent mutations |
Stripe PaymentIntent Lifecycle
Section titled “Stripe PaymentIntent Lifecycle”Each billing window creates one Stripe PaymentIntent. The state machine:
requires_payment_method | v requires_confirmation --> cancel (call never started) | v requires_capture --> cancel (window released) | v succeeded (window captured)| State | Meaning |
|---|---|
requires_payment_method | Initial hold attempt pending |
requires_capture | Hold authorized --- funds reserved on guest’s card |
succeeded | Captured --- funds permanently charged |
canceled | Hold released --- no charge |
Billing Orchestrator
Section titled “Billing Orchestrator”The billing orchestrator runs server-side and manages window transitions. It is implemented as a state machine coordinated by the signaling service:
Call connects (both peers joined) | v Start billing timer (Redis) | | At window_size - 60s v Place next window hold | | At window_size boundary v Capture current window | | Repeat for each window v Call ends | v Capture partial window Cancel remaining holds Finalize call session recordRedis State
Section titled “Redis State”| Key | TTL | Content |
|---|---|---|
billing:{session_id}:state | Call duration + 1h | Current window, start time, customer ID |
billing:{session_id}:pi:{window} | Call duration + 1h | PaymentIntent ID for each window |
Platform Fee
Section titled “Platform Fee”The platform deducts a 7% fee from each captured payment before crediting the host’s balance.
Calculation:
host_earnings = captured_amount x (1 - 0.07)platform_fee = captured_amount x 0.07Example: Guest pays $20.00 for a 10-minute session at $2.00/min.
- Platform fee: $20.00 x 0.07 = $1.40
- Host earnings: $20.00 - $1.40 = $18.60
Payout Flow
Section titled “Payout Flow” Host requests payout | v Validate: amount <= available balance | v Create Stripe Payout to host's bank | v Payout record created (status: pending) | | Stripe webhook: payout.paid v Payout record updated (status: completed)Hosts add their bank account via Stripe’s onboarding flow. The platform creates a Stripe Custom connected account for each host. Payouts are sent from the platform’s Stripe balance to the host’s bank account.
Invoice Generation
Section titled “Invoice Generation”When a payment is captured, a PDF invoice is automatically generated:
- Invoice data is assembled (payer, host, amount, date, session details)
- PDF is rendered using
gofpdf - PDF is uploaded to Cloudflare R2 at
invoices/{year}/{month}/{invoice_id}.pdf - Invoice record is stored in the database
- Download is available via presigned URL (1-hour expiry)
This process runs asynchronously (fire-and-forget). Invoice generation failures are logged but do not block the payment capture.
Payment Notifications
Section titled “Payment Notifications”On successful payment capture, two notifications are sent:
- In-app notification: Created in the notifications table with type
payment_captured - Email receipt: Sent via Resend with the invoice download link
Both are fire-and-forget operations --- failures are logged but do not affect the payment flow.
Webhook Processing
Section titled “Webhook Processing”Vidivo listens to Stripe webhooks for asynchronous payment events:
| Event | Handler Action |
|---|---|
payment_intent.succeeded | Safety net: marks transaction as succeeded if not already |
payment_intent.payment_failed | Marks transaction as failed, notifies host |
payout.paid | Marks payout as completed with timestamp |
payout.failed | Marks payout as failed, records failure reason, notifies host |
All webhooks are verified using Stripe-Signature header validation with the webhook signing secret.
Data Export
Section titled “Data Export”Payment and payout history can be exported in three formats:
| Format | Content-Type | Library |
|---|---|---|
| CSV | text/csv | Standard library encoding/csv |
| XLSX | application/vnd.openxmlformats-officedocument.spreadsheetml.sheet | excelize/v2 |
application/pdf | gofpdf |
Exports support date range filtering (start_date, end_date) and status filtering.