Calls
The Calls API manages call links, session lifecycle, call history, and scheduling. Call links are managed under /v1/calls/links/, sessions under /v1/calls/, and scheduling under /v1/schedule/.
Call Lifecycle
Section titled “Call Lifecycle” WAITING --> RINGING --> ACTIVE --> COMPLETED | | +-- MISSED (host didn't | join within 5 min) FAILED (billing error)| State | Description |
|---|---|
waiting | Guest joined; hold placed; waiting for host |
ringing | Host notified of incoming call |
active | Both peers connected, timer running |
completed | Session finished, billing settled |
missed | Host did not join within the no-show threshold (5 minutes) |
failed | Billing error prevented completion |
Call Links
Section titled “Call Links”POST /v1/calls/links
Section titled “POST /v1/calls/links”Create a new call link. Only hosts can create links.
Authentication: Bearer token with host or admin role.
Request
Section titled “Request”POST /v1/calls/linksAuthorization: Bearer eyJhbGciOiJSUzI1NiJ9...Content-Type: application/json{ "rate_per_minute": "2.50", "window_size_minutes": 10, "expires_at": "2026-03-22T00:00:00Z", "max_uses": 1, "label": "Session with Alex"}| Field | Type | Required | Description |
|---|---|---|---|
rate_per_minute | string | Yes | Per-minute charge in USD. Min $0.50, max $999.99. |
window_size_minutes | integer | Yes | Billing window size (1—60 minutes) |
expires_at | string (ISO 8601) | No | When the link expires. Null = never. |
max_uses | integer | No | Max uses. Null = unlimited. |
label | string | No | Internal label. Max 120 characters. |
Response
Section titled “Response”HTTP/1.1 201 Created{ "id": "01JN2X7N1Q6I0TSCUYZ9AELGHI", "short_code": "abc123", "url": "https://vidivo.app/c/abc123", "host_id": "01JN2X4K8M3F7QPZRVWT6YBHCE", "rate_per_minute": "2.50", "window_size_minutes": 10, "expires_at": "2026-03-22T00:00:00Z", "max_uses": 1, "use_count": 0, "label": "Session with Alex", "is_active": true, "created_at": "2026-03-15T10:00:00Z"}Errors
Section titled “Errors”| Code | Status | Description |
|---|---|---|
forbidden | 403 | User is not a host |
validation_error | 400 | Invalid rate or window size |
GET /v1/calls/links
Section titled “GET /v1/calls/links”List all call links for the authenticated host.
Authentication: Bearer token with host or admin role.
GET /v1/calls/linksAuthorization: Bearer eyJhbGciOiJSUzI1NiJ9...Returns an array of call link objects.
DELETE /v1/calls/links/:id
Section titled “DELETE /v1/calls/links/:id”Deactivate a call link. The link is soft-deleted and cannot be used for new calls.
Authentication: Bearer token with host or admin role. Must own the link.
DELETE /v1/calls/links/01JN2X7N1Q6I0TSCUYZ9AELGHIAuthorization: Bearer eyJhbGciOiJSUzI1NiJ9...Returns 204 No Content on success.
GET /v1/calls/links/:shortCode/info
Section titled “GET /v1/calls/links/:shortCode/info”Retrieve public link information for the guest join page. No authentication required.
GET /v1/calls/links/abc123/info{ "short_code": "abc123", "host": { "display_name": "Jane Smith", "avatar_url": "https://cdn.vidivo.app/avatars/01JN2X4K.jpg", "username": "janecoach" }, "rate_per_minute": "2.50", "window_size_minutes": 10, "is_active": true}GET /v1/calls/links/active/:username
Section titled “GET /v1/calls/links/active/:username”Get the host’s most recent active call link by username. Public endpoint.
GET /v1/calls/links/active/janecoachCall Sessions
Section titled “Call Sessions”POST /v1/calls/join/:short_code
Section titled “POST /v1/calls/join/:short_code”Join a call via a link. This is called by the guest (or authenticated user) to initiate a call session. The endpoint validates the link, creates a call session, and optionally places a Stripe hold for the first billing window.
Authentication: Guest token or user access token.
Request
Section titled “Request”POST /v1/calls/join/abc123Content-Type: application/jsonX-Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000{ "payment_method_id": "pm_1OqBuv2eZvKYlo2C8XXXXXXXXXXX", "guest_name": "Alex", "guest_email": "alex@example.com"}| Field | Type | Required | Description |
|---|---|---|---|
payment_method_id | string | Yes | Stripe PaymentMethod ID from Stripe Elements |
guest_name | string | No | Display name for the guest |
guest_email | string | No | Guest email for receipt |
Response
Section titled “Response”HTTP/1.1 201 Created{ "call": { "id": "01JN2X6M0P5H9SRBTXY8ZDKEFG", "status": "waiting", "short_code": "abc123", "host": { "id": "01JN2X4K8M3F7QPZRVWT6YBHCE", "display_name": "Jane Smith" }, "rate_per_minute": "2.50", "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 |
|---|---|---|
not_found | 404 | Short code does not exist or link expired |
conflict | 409 | Link has reached its maximum use count |
stripe_error | 402 | Stripe hold could not be placed |
POST /v1/calls/:id/host-join
Section titled “POST /v1/calls/:id/host-join”The host joins their own call session after a guest has initiated it.
Authentication: Bearer token required. Must be the call’s host.
POST /v1/calls/01JN2X6M0P5H9SRBTXY8ZDKEFG/host-joinAuthorization: Bearer eyJhbGciOiJSUzI1NiJ9...Response
Section titled “Response”HTTP/1.1 201 Created{ "call": { "id": "01JN2X6M0P5H9SRBTXY8ZDKEFG", "status": "active" }, "signaling_token": "st_host_01JN2X6M0P5H9SRBTXY8ZDKEFG", "turn_credentials": { "urls": ["turn:turn.vidivo.app:3478", "turns:turn.vidivo.app:5349"], "username": "1710090000:01JN2X4K8M3F7QPZRVWT6YBHCE", "credential": "hmac-sha1-credential-here" }}POST /v1/calls/:id/join
Section titled “POST /v1/calls/:id/join”Join a scheduled call. Both host and guest use this endpoint to join a previously booked call.
Authentication: Bearer token required.
POST /v1/calls/01JN2X6M0P5H9SRBTXY8ZDKEFG/joinAuthorization: Bearer eyJhbGciOiJSUzI1NiJ9...POST /v1/calls/:id/end
Section titled “POST /v1/calls/:id/end”End an active or waiting call session. Either the host or guest may call this endpoint.
Authentication: Bearer token required.
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”{ "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 |
conflict | 409 | Call is not in an endable state |
GET /v1/calls/:id
Section titled “GET /v1/calls/:id”Retrieve call session details.
Authentication: Bearer token required.
GET /v1/calls/01JN2X6M0P5H9SRBTXY8ZDKEFGAuthorization: Bearer eyJhbGciOiJSUzI1NiJ9...GET /v1/calls/history
Section titled “GET /v1/calls/history”Retrieve call history for the authenticated user. Supports filtering by status.
Authentication: Bearer token required.
GET /v1/calls/history?page=1&per_page=20&status=completedAuthorization: Bearer eyJhbGciOiJSUzI1NiJ9...| Query Parameter | Type | Description |
|---|---|---|
page | integer | Page number (default: 1) |
per_page | integer | Results per page (default: 20) |
status | string | Filter by status: waiting, ringing, active, completed, missed, failed |
Scheduling
Section titled “Scheduling”GET /v1/schedule/:username
Section titled “GET /v1/schedule/:username”Get a host’s available time slots for a date range. Public endpoint.
GET /v1/schedule/janecoach?start_date=2026-03-15&end_date=2026-03-22| Query Parameter | Type | Required | Description |
|---|---|---|---|
start_date | string | Yes | Start date (YYYY-MM-DD) |
end_date | string | Yes | End date (YYYY-MM-DD) |
Response
Section titled “Response”{ "host": { "display_name": "Jane Smith", "username": "janecoach", "rate_per_minute": "2.50", "window_size_minutes": 10 }, "slots": [ { "date": "2026-03-15", "start_time": "09:00", "end_time": "10:00", "available": true }, { "date": "2026-03-15", "start_time": "10:00", "end_time": "11:00", "available": false } ]}POST /v1/schedule/book
Section titled “POST /v1/schedule/book”Book a scheduled call with a host.
Authentication: Bearer token required.
POST /v1/schedule/bookAuthorization: Bearer eyJhbGciOiJSUzI1NiJ9...Content-Type: application/json{ "host_username": "janecoach", "scheduled_at": "2026-03-16T14:00:00Z", "duration_minutes": 30, "note": "Career coaching session"}Errors
Section titled “Errors”| Code | Status | Description |
|---|---|---|
not_found | 404 | Host not found |
conflict | 409 | Time slot already booked |
validation_error | 400 | Time is outside host’s availability |
GET /v1/schedule/mine
Section titled “GET /v1/schedule/mine”List the authenticated user’s scheduled calls.
Authentication: Bearer token required.
GET /v1/schedule/mine?page=1&per_page=20Authorization: Bearer eyJhbGciOiJSUzI1NiJ9...PUT /v1/schedule/:id/cancel
Section titled “PUT /v1/schedule/:id/cancel”Cancel a scheduled call. Both host and guest can cancel.
Authentication: Bearer token required.
PUT /v1/schedule/01JN2X6M0P5H9SRBTXY8ZDKEFG/cancelAuthorization: Bearer eyJhbGciOiJSUzI1NiJ9...Content-Type: application/json{ "reason": "Schedule conflict"}Returns 204 No Content on success.
GET /v1/schedule/:id/calendar
Section titled “GET /v1/schedule/:id/calendar”Download an .ics calendar invite file for a scheduled call.
Authentication: Bearer token required.
GET /v1/schedule/01JN2X6M0P5H9SRBTXY8ZDKEFG/calendarAuthorization: Bearer eyJhbGciOiJSUzI1NiJ9...Returns an text/calendar file download with Content-Disposition: attachment; filename=vidivo-call.ics.
WebSocket Signaling
Section titled “WebSocket Signaling”WebRTC signaling is handled over a WebSocket connection separate from the REST API.
Endpoint: wss://signal.vidivo.app/signal/ws?token=<signaling_token>
Obtain the signaling_token from POST /v1/calls/join/:short_code or POST /v1/calls/:id/host-join. Tokens are valid for 5 minutes.
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 to Host | WebRTC SDP offer |
answer | Host to Guest | WebRTC SDP answer |
ice_candidate | Both | ICE candidate for NAT traversal |
peer_joined | Server to Both | Other participant connected |
peer_left | Server to Both | Other participant disconnected |
error | Server to Client | Signaling error |
Code Examples
Section titled “Code Examples”# Create a call linkcurl -X POST https://api.vidivo.app/v1/calls/links \ -H "Authorization: Bearer eyJhbGciOiJSUzI1NiJ9..." \ -H "Content-Type: application/json" \ -d '{ "rate_per_minute": "2.50", "window_size_minutes": 10, "max_uses": 1, "label": "Session with Alex" }'
# Join a call via linkcurl -X POST https://api.vidivo.app/v1/calls/join/abc123 \ -H "Content-Type: application/json" \ -H "X-Idempotency-Key: $(uuidgen)" \ -d '{ "payment_method_id": "pm_1OqBuv2eZvKYlo2C8XXXXXXXXXXX", "guest_name": "Alex" }'
# Get call historycurl "https://api.vidivo.app/v1/calls/history?status=completed" \ -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"}'
# Book a scheduled callcurl -X POST https://api.vidivo.app/v1/schedule/book \ -H "Authorization: Bearer eyJhbGciOiJSUzI1NiJ9..." \ -H "Content-Type: application/json" \ -d '{ "host_username": "janecoach", "scheduled_at": "2026-03-16T14:00:00Z", "duration_minutes": 30 }'const API = 'https://api.vidivo.app/v1';
// Create a call linkconst { data: link } = await fetch(`${API}/calls/links`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${accessToken}`, }, body: JSON.stringify({ rate_per_minute: '2.50', window_size_minutes: 10, max_uses: 1, }),}).then(r => r.json());
// Join a callconst { data } = await fetch(`${API}/calls/join/abc123`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Idempotency-Key': crypto.randomUUID(), }, body: JSON.stringify({ payment_method_id: 'pm_1OqBuv2eZvKYlo2C8XXXXXXXXXXX', guest_name: 'Alex', }),}).then(r => r.json());
// Connect WebSocket signalingconst ws = new WebSocket( `wss://signal.vidivo.app/signal/ws?token=${data.signaling_token}`);
ws.onmessage = (event) => { const { type, payload } = JSON.parse(event.data); switch (type) { case 'peer_joined': console.log('Peer connected'); break; case 'offer': // Handle SDP offer break; case 'ice_candidate': peerConnection.addIceCandidate(payload); break; }};