Calls
The Calls API manages the lifecycle of video call sessions — from initiation through signaling to termination and billing capture.
Call Lifecycle
Section titled “Call Lifecycle” PENDING ──► WAITING ──► ACTIVE ──► ENDING ──► COMPLETED │ │ └── CANCELLED (link expired/ │ guest didn't show) │ FAILED (billing error)| State | Description |
|---|---|
pending | Call link was opened; guest is entering payment details |
waiting | First hold placed; waiting for host to join |
active | Both peers connected, timer running |
ending | Either party clicked End; billing capture in progress |
completed | Session finished, billing settled |
cancelled | Guest left before connecting, or link expired |
failed | Billing error prevented completion |
POST /calls/initiate
Section titled “POST /calls/initiate”Begin a call session. This endpoint:
- Validates the call link (
short_code) - Places a Stripe PaymentIntent hold for Window 1
- Creates a call session record
- Issues a WebSocket signaling token
Authentication: Requires a guest token or user access token.
Request
Section titled “Request”POST /v1/calls/initiateContent-Type: application/jsonX-Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000{ "short_code": "abc123", "payment_method_id": "pm_1OqBuv2eZvKYlo2C8XXXXXXXXXXX", "guest_name": "Alex"}| Field | Type | Required | Description |
|---|---|---|---|
short_code | string | Yes | The call link short code from the URL |
payment_method_id | string | Yes | Stripe PaymentMethod ID from Stripe Elements |
guest_name | string | No | Display name for the guest |
Response
Section titled “Response”HTTP/1.1 201 CreatedContent-Type: application/json{ "call": { "id": "01JN2X6M0P5H9SRBTXY8ZDKEFG", "status": "waiting", "short_code": "abc123", "host": { "id": "01JN2X4K8M3F7QPZRVWT6YBHCE", "display_name": "Jane Smith" }, "rate_per_minute": "2.00", "window_size_minutes": 10, "created_at": "2026-03-15T14:00:00Z" }, "signaling_token": "st_01JN2X6M0P5H9SRBTXY8ZDKEFG", "turn_credentials": { "urls": ["turn:turn.vidivo.app:3478", "turns:turn.vidivo.app:5349"], "username": "1710090000:01JN2X6M0P5H9SRBTXY8ZDKEFG", "credential": "hmac-sha1-credential-here" }}Errors
Section titled “Errors”| Code | Status | Description |
|---|---|---|
link_not_found | 404 | Short code does not exist or link is expired |
link_exhausted | 409 | Link has reached its maximum use count |
payment_failed | 402 | Stripe hold could not be placed |
host_unavailable | 503 | Host is not currently available |
POST /calls/{id}/end
Section titled “POST /calls/{id}/end”End an active or waiting call session. Either the host or guest may call this endpoint.
Authentication: Bearer token for the authenticated user, or guest token.
Request
Section titled “Request”POST /v1/calls/01JN2X6M0P5H9SRBTXY8ZDKEFG/endAuthorization: Bearer eyJhbGciOiJSUzI1NiJ9...Content-Type: application/jsonX-Idempotency-Key: 550e8400-e29b-41d4-a716-446655440001{ "reason": "user_ended"}| Field | Type | Required | Description |
|---|---|---|---|
reason | string | No | user_ended, host_ended, timeout, billing_error |
Response
Section titled “Response”HTTP/1.1 200 OKContent-Type: application/json{ "call": { "id": "01JN2X6M0P5H9SRBTXY8ZDKEFG", "status": "completed", "duration_seconds": 450, "billed_minutes": 8, "total_charged": "16.00", "currency": "usd", "ended_at": "2026-03-15T14:07:30Z" }}Errors
Section titled “Errors”| Code | Status | Description |
|---|---|---|
not_found | 404 | Call session not found |
forbidden | 403 | Caller is not a participant in this call |
invalid_state | 409 | Call is not in an endable state |
GET /calls/{id}/status
Section titled “GET /calls/{id}/status”Retrieve the current status and metadata for a call session.
Authentication: Bearer token (host or guest of the session).
Request
Section titled “Request”GET /v1/calls/01JN2X6M0P5H9SRBTXY8ZDKEFG/statusAuthorization: Bearer eyJhbGciOiJSUzI1NiJ9...Response
Section titled “Response”HTTP/1.1 200 OKContent-Type: application/json{ "call": { "id": "01JN2X6M0P5H9SRBTXY8ZDKEFG", "status": "active", "short_code": "abc123", "host": { "id": "01JN2X4K8M3F7QPZRVWT6YBHCE", "display_name": "Jane Smith" }, "guest_name": "Alex", "rate_per_minute": "2.00", "window_size_minutes": 10, "current_window": 1, "window_start_at": "2026-03-15T14:00:30Z", "next_hold_at": "2026-03-15T14:09:30Z", "elapsed_seconds": 270, "estimated_charge": "9.00", "created_at": "2026-03-15T14:00:00Z", "started_at": "2026-03-15T14:00:30Z" }}WebSocket Signaling
Section titled “WebSocket Signaling”WebRTC signaling is handled over a WebSocket connection separate from the REST API.
Endpoint: wss://signal.vidivo.app/ws?token=<signaling_token>
Obtain the signaling_token from POST /calls/initiate. Tokens are valid for 5 minutes and must be used immediately after initiating a call.
Message Format
Section titled “Message Format”All WebSocket messages use JSON:
{ "type": "offer | answer | ice_candidate | peer_joined | peer_left | error", "payload": { ... }}Signaling Flow
Section titled “Signaling Flow” Guest Signal Server Host │ │ │ │── connect(token) ────────►│ │ │ │ │ │ │◄── connect(token) ─────│ │ │ │ │ │──── peer_joined ───────►│ │◄──── peer_joined ─────────│ │ │ │ │ │──── offer (SDP) ─────────►│ │ │ │──── offer (SDP) ───────►│ │ │ │ │ │◄── answer (SDP) ────────│ │◄─── answer (SDP) ─────────│ │ │ │ │ │── ice_candidate ─────────►│──── ice_candidate ─────►│ │◄─ ice_candidate ──────────│◄─── ice_candidate ──────│ │ │ │ │════════════════ P2P WebRTC established ════════════│Message Types
Section titled “Message Types”| Type | Direction | Description |
|---|---|---|
offer | Guest → Host | WebRTC SDP offer |
answer | Host → Guest | WebRTC SDP answer |
ice_candidate | Both | ICE candidate for NAT traversal |
peer_joined | Server → Both | Other participant connected |
peer_left | Server → Both | Other participant disconnected |
error | Server → Client | Signaling error |
Code Examples
Section titled “Code Examples”# Initiate a callcurl -X POST https://api.vidivo.app/v1/calls/initiate \ -H "Content-Type: application/json" \ -H "X-Idempotency-Key: $(uuidgen)" \ -d '{ "short_code": "abc123", "payment_method_id": "pm_1OqBuv2eZvKYlo2C8XXXXXXXXXXX", "guest_name": "Alex" }'
# Get call statuscurl https://api.vidivo.app/v1/calls/01JN2X6M0P5H9SRBTXY8ZDKEFG/status \ -H "Authorization: Bearer eyJhbGciOiJSUzI1NiJ9..."
# End a callcurl -X POST https://api.vidivo.app/v1/calls/01JN2X6M0P5H9SRBTXY8ZDKEFG/end \ -H "Authorization: Bearer eyJhbGciOiJSUzI1NiJ9..." \ -H "Content-Type: application/json" \ -H "X-Idempotency-Key: $(uuidgen)" \ -d '{"reason":"user_ended"}'const API = 'https://api.vidivo.app/v1';
// Initiate callconst { call, signaling_token, turn_credentials } = await fetch(`${API}/calls/initiate`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Idempotency-Key': crypto.randomUUID(), }, body: JSON.stringify({ short_code: 'abc123', payment_method_id: 'pm_1OqBuv2eZvKYlo2C8XXXXXXXXXXX', guest_name: 'Alex', }),}).then(r => r.json());
// Connect WebSocket signalingconst ws = new WebSocket(`wss://signal.vidivo.app/ws?token=${signaling_token}`);
ws.onmessage = (event) => { const { type, payload } = JSON.parse(event.data); if (type === 'offer') { // Handle incoming SDP offer from host } else if (type === 'ice_candidate') { peerConnection.addIceCandidate(payload); }};
// Create RTCPeerConnection with TURN credentialsconst peerConnection = new RTCPeerConnection({ iceServers: [ { urls: turn_credentials.urls, username: turn_credentials.username, credential: turn_credentials.credential, }, ],});package main
import ( "bytes" "encoding/json" "fmt" "net/http"
"github.com/google/uuid")
type InitiateCallRequest struct { ShortCode string `json:"short_code"` PaymentMethodID string `json:"payment_method_id"` GuestName string `json:"guest_name,omitempty"`}
func initiateCall(token, shortCode, paymentMethodID string) error { body, _ := json.Marshal(InitiateCallRequest{ ShortCode: shortCode, PaymentMethodID: paymentMethodID, GuestName: "Alex", })
req, _ := http.NewRequest("POST", "https://api.vidivo.app/v1/calls/initiate", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("X-Idempotency-Key", uuid.New().String())
resp, err := http.DefaultClient.Do(req) if err != nil { return err } defer resp.Body.Close()
fmt.Printf("Status: %s\n", resp.Status) return nil}