Payments
Vidivo uses Stripe Connect Express with a time-window hold/capture model. Guests pay per minute; hosts receive payouts minus a 7% platform fee.
Time-Window Billing Model
Section titled “Time-Window Billing Model”The billing model is designed to pre-authorize funds before they are spent, preventing unpayable debts at call end.
How It Works
Section titled “How It Works” Time: 0 10 20 30 min │ │ │ │ ▼ ▼ ▼ ▼ Holds: [─W1─────][─W2─────][─W3────] ↑ ↑ W1 captured W2 captured
At call end (e.g. 24 min): ├── W3 partial: 4 min × rate = captured └── Remainder of W3 hold: releasedStep-by-step:
- Guest submits payment method. Stripe hold placed for Window 1 (
window_size × rate). - At
window_size - 60 seconds, hold for Window 2 is placed. - At the
window_sizeboundary, Window 1 is captured (charged permanently). - This repeats for every subsequent window.
- When the call ends:
- The partial final window is captured:
⌈elapsed_minutes⌉ × rate - All remaining holds are released immediately.
- The partial final window is captured:
Example
Section titled “Example”Host rate: $3.00/min, window size: 10 minutes.
| Time | Action | Stripe Operation |
|---|---|---|
| T+0s | Call starts | Hold $30.00 (W1) |
| T+9m | 60s before W1 ends | Hold $30.00 (W2) |
| T+10m | W1 boundary | Capture $30.00 (W1) |
| T+19m | 60s before W2 ends | Hold $30.00 (W3) |
| T+20m | W2 boundary | Capture $30.00 (W2) |
| T+23m30s | Guest ends call | Capture $12.00 (4 min rounded up × $3), Release $18.00 |
Total charged: $72.00 | Host receives: $72.00 × 0.93 = $66.96
Idempotency
Section titled “Idempotency”All payment mutations use Stripe Idempotency Keys to prevent duplicate charges. Always pass X-Idempotency-Key on:
POST /calls/initiate(first hold)POST /calls/{id}/end(final capture)
GET /payments/history
Section titled “GET /payments/history”Retrieve payment history for the authenticated user.
Authentication: Bearer token (host or guest).
Behavior:
- Hosts see their earning records — amounts after platform fee deduction.
- Guests see their charge records — what was captured from their card.
Request
Section titled “Request”GET /v1/payments/history?limit=20&cursor=eyJpZCI6IjEyMyJ9Authorization: Bearer eyJhbGciOiJSUzI1NiJ9...| Query Parameter | Type | Description |
|---|---|---|
limit | integer | Results per page. Default 20, max 100. |
cursor | string | Pagination cursor from previous response. |
from | string (ISO 8601) | Filter: only transactions after this date. |
to | string (ISO 8601) | Filter: only transactions before this date. |
Response
Section titled “Response”HTTP/1.1 200 OKContent-Type: application/json{ "transactions": [ { "id": "01JN2X8O2R7J1UDVY0AB1BFMJK", "call_session_id": "01JN2X6M0P5H9SRBTXY8ZDKEFG", "type": "capture", "window_number": 1, "amount": "30.00", "currency": "usd", "status": "succeeded", "stripe_payment_intent_id": "pi_1OqBuv2eZvKYlo2CXXXXXXXXXX", "created_at": "2026-03-15T14:10:00Z" }, { "id": "01JN2X9P3S8K2VEWZ1BC2CGNLM", "call_session_id": "01JN2X6M0P5H9SRBTXY8ZDKEFG", "type": "capture", "window_number": 2, "amount": "12.00", "currency": "usd", "status": "succeeded", "stripe_payment_intent_id": "pi_2PrCvw3fAuZMmp3DXXXXXXXXXXX", "created_at": "2026-03-15T14:23:30Z" } ], "summary": { "total_amount": "42.00", "currency": "usd", "transaction_count": 2 }, "pagination": { "cursor": null, "has_more": false, "limit": 20 }}Stripe Connect Setup
Section titled “Stripe Connect Setup”Hosts must connect a Stripe account before receiving payouts.
Onboarding Flow
Section titled “Onboarding Flow”POST /v1/stripe/connect/onboardAuthorization: Bearer eyJhbGciOiJSUzI1NiJ9...Response:
{ "onboarding_url": "https://connect.stripe.com/setup/e/acct_XXXX/..."}Redirect the host to onboarding_url. Stripe handles all identity verification and bank account linking. After completion, Stripe redirects back to Vidivo.
Check Stripe Status
Section titled “Check Stripe Status”GET /v1/stripe/connect/statusAuthorization: Bearer eyJhbGciOiJSUzI1NiJ9...{ "stripe_account_id": "acct_1XXXXXXXXXXXXXXXXX", "charges_enabled": true, "payouts_enabled": true, "details_submitted": true, "requirements": []}When charges_enabled and payouts_enabled are both true, the host can receive calls.
Webhook Events
Section titled “Webhook Events”Vidivo listens to Stripe webhooks to maintain billing state. All webhooks are verified using Stripe’s webhook signing secret.
Handled Events
Section titled “Handled Events”| Event | Description |
|---|---|
payment_intent.succeeded | Hold or capture succeeded |
payment_intent.payment_failed | Payment failed — triggers call termination |
account.updated | Host’s Stripe account status changed |
payout.paid | Host payout sent to bank |
payout.failed | Host payout failed — triggers notification |
Webhook Endpoint
Section titled “Webhook Endpoint”POST https://api.vidivo.app/v1/stripe/webhookAll requests are verified using Stripe-Signature header validation. Do not route Stripe webhooks through Cloudflare proxy — use the direct IP or a trusted proxy.
Webhook Payload Example
Section titled “Webhook Payload Example”{ "id": "evt_1OqBuv2eZvKYlo2CXXXXXXXXXX", "object": "event", "type": "payment_intent.succeeded", "data": { "object": { "id": "pi_1OqBuv2eZvKYlo2CXXXXXXXXXX", "amount": 3000, "currency": "usd", "status": "succeeded", "metadata": { "call_session_id": "01JN2X6M0P5H9SRBTXY8ZDKEFG", "window_number": "1", "type": "capture" } } }}Hold/Capture Lifecycle
Section titled “Hold/Capture Lifecycle”Each billing window creates one Stripe PaymentIntent. The state machine for a single PaymentIntent:
requires_payment_method │ ▼ requires_confirmation ──► cancel (call never started) │ ▼ requires_capture ──► cancel (window released) │ ▼ succeeded (window captured)| State | Meaning |
|---|---|
requires_payment_method | Initial hold attempt pending |
requires_capture | Hold authorized — funds reserved |
succeeded | Captured — funds permanently charged |
canceled | Hold released — no charge |
Partial Window Capture
Section titled “Partial Window Capture”When a call ends mid-window, a partial amount is captured:
partial_amount = ceil(elapsed_minutes_in_window) × rate_per_minuteremaining_amount = window_hold_amount - partial_amountThe PaymentIntent is captured for partial_amount. Stripe releases remaining_amount automatically.
Code Examples
Section titled “Code Examples”# Get payment historycurl "https://api.vidivo.app/v1/payments/history?limit=20" \ -H "Authorization: Bearer eyJhbGciOiJSUzI1NiJ9..."
# Start Stripe Connect onboardingcurl -X POST https://api.vidivo.app/v1/stripe/connect/onboard \ -H "Authorization: Bearer eyJhbGciOiJSUzI1NiJ9..."
# Check Stripe account statuscurl https://api.vidivo.app/v1/stripe/connect/status \ -H "Authorization: Bearer eyJhbGciOiJSUzI1NiJ9..."const API = 'https://api.vidivo.app/v1';
// Get payment historyconst { transactions, summary } = await fetch(`${API}/payments/history`, { headers: { Authorization: `Bearer ${accessToken}` },}).then(r => r.json());
console.log(`Total earned: $${summary.total_amount}`);
// Start Stripe Connect onboardingconst { onboarding_url } = await fetch(`${API}/stripe/connect/onboard`, { method: 'POST', headers: { Authorization: `Bearer ${accessToken}` },}).then(r => r.json());
// Redirect to Stripewindow.location.href = onboarding_url;func getPaymentHistory(accessToken string) error { req, _ := http.NewRequest("GET", "https://api.vidivo.app/v1/payments/history?limit=20", nil) req.Header.Set("Authorization", "Bearer "+accessToken)
resp, err := http.DefaultClient.Do(req) if err != nil { return err } defer resp.Body.Close()
var result map[string]interface{} json.NewDecoder(resp.Body).Decode(&result)
summary := result["summary"].(map[string]interface{}) fmt.Printf("Total: $%s %s\n", summary["total_amount"], summary["currency"]) return nil}