Skip to content

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.

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.

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: released
  1. Guest submits payment: A Stripe PaymentMethod is created via Stripe Elements on the client. Card data never touches Vidivo servers.

  2. Window 1 hold: The billing service creates a PaymentIntent with capture_method: manual for window_size x rate_per_minute. This places an authorization hold on the guest’s card.

  3. 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.

  4. Window capture (at window_size): The current window’s PaymentIntent is captured. The full window amount is charged.

  5. 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.

  6. Hold release: Any future window holds that were not captured are canceled, releasing the authorization immediately.

ConcernSolution
Guest runs out of funds mid-callPre-authorization detects insufficient funds before the call starts
Guest disputes charges for unused timeOnly actual usage is captured; holds are released
Host is not paid for timeCaptures happen at window boundaries, not at call end
Network failure during callBilling timers run server-side (Redis), independent of client connections
Duplicate chargesStripe Idempotency Keys on all PaymentIntent mutations

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)
StateMeaning
requires_payment_methodInitial hold attempt pending
requires_captureHold authorized --- funds reserved on guest’s card
succeededCaptured --- funds permanently charged
canceledHold released --- no charge

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 record
KeyTTLContent
billing:{session_id}:stateCall duration + 1hCurrent window, start time, customer ID
billing:{session_id}:pi:{window}Call duration + 1hPaymentIntent ID for each window

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.07

Example: 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
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.

When a payment is captured, a PDF invoice is automatically generated:

  1. Invoice data is assembled (payer, host, amount, date, session details)
  2. PDF is rendered using gofpdf
  3. PDF is uploaded to Cloudflare R2 at invoices/{year}/{month}/{invoice_id}.pdf
  4. Invoice record is stored in the database
  5. 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.

On successful payment capture, two notifications are sent:

  1. In-app notification: Created in the notifications table with type payment_captured
  2. 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.

Vidivo listens to Stripe webhooks for asynchronous payment events:

EventHandler Action
payment_intent.succeededSafety net: marks transaction as succeeded if not already
payment_intent.payment_failedMarks transaction as failed, notifies host
payout.paidMarks payout as completed with timestamp
payout.failedMarks payout as failed, records failure reason, notifies host

All webhooks are verified using Stripe-Signature header validation with the webhook signing secret.

Payment and payout history can be exported in three formats:

FormatContent-TypeLibrary
CSVtext/csvStandard library encoding/csv
XLSXapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheetexcelize/v2
PDFapplication/pdfgofpdf

Exports support date range filtering (start_date, end_date) and status filtering.