First call in 5 minutes
Two API calls. Two URLs. That's the entire integration.
Get your API key
Sign up → create a project → copy your key (starts with bj_live_).
Install the SDK
npm install @purplecallio/sdk
Create a call from your backend
import PurpleCallio from '@purplecallio/sdk';
const bj = new PurpleCallio({ apiKey: process.env.PURPLECALLIO_API_KEY });
const { callId, callerUrl, receiverUrl } = await bj.createCall({
callerId: 'user_alice',
receiverId: 'user_bob',
});Redirect each user — done
Alice opens callerUrl, Bob opens receiverUrl. PurpleCallio handles the rest.
How it works
Your backend
POST /calls
PurpleCallio
Returns 2 URLs
Alice (caller)
Opens callerUrl
Bob (receiver)
Opens receiverUrl
Three types of credentials
bj_live_...API Key
Your server → REST API
bj_session_...Session Token
Browser → WebSocket
JWT BearerDashboard JWT
Dashboard UI → management API
Authentication
Every REST request needs your API key in the x-api-key header.
x-api-key: $PURPLECALLIO_API_KEY
callerUrl / receiverUrl are single-use. Do not cache or reuse them.Hosted UI — zero frontend work
The fastest way to integrate. Create a call from your backend, then redirect your users to a PurpleCallio-hosted meeting page. No frontend implementation required.
Integration flow
Your backend
POST /calls
PurpleCallio
Returns hostedUrl + tokens
Redirect users
Meeting starts
Branding
Configure branding on your project and the hosted page will apply it automatically.
{
"branding": {
"companyName": "Acme",
"logoUrl": "https://cdn.acme.com/logo.png",
"primaryColor": "#2563EB"
},
"theme": "dark",
"waitingRoom": true
}⚛️ React UI Components
Build a custom interface with reusable React components — no need to implement WebRTC, signaling, or media handling yourself.
npm install @purplecallio/react
Quick example
import { MeetingProvider, MeetingRoom, ParticipantGrid, ControlBar } from '@purplecallio/react';
export function Call({ token, callId, signalUrl }) {
return (
<MeetingProvider token={token} callId={callId} signalUrl={signalUrl}>
<MeetingRoom>
<ParticipantGrid />
</MeetingRoom>
<ControlBar />
</MeetingProvider>
);
}MeetingProviderContext provider — wires the engine, media, and signaling
MeetingRoomMeeting layout shell with waiting room support
ParticipantGrid / ParticipantTileGrid of participant video tiles
ActiveSpeakerViewLarge speaker view + local PiP
CameraButton / MicrophoneButtonToggle camera / microphone
ScreenShareButton / LeaveButtonScreen share + end call controls
DeviceSelectorCamera / microphone / speaker picker
WaitingRoomWaiting room panel
ConnectionStatusLive connection state indicator
SpeakingIndicatorActive speaker indicator
Hooks
import { useMeeting, useParticipants, useParticipant, useDevices, useConnection } from '@purplecallio/react';
function Status() {
const { connectionState } = useConnection();
const participants = useParticipants();
const { toggleCamera, toggleMicrophone } = useMeeting();
const devices = useDevices();
return null;
}@purplecallio/react is built on top of @purplecallio/sdk — the same session tokens and signaling work across both.Headless SDK
For developers who want complete control. PurpleCallio provides only the communication engine — no UI included.
npm install @purplecallio/sdk
Quick example
import { PurpleCallioMeeting } from '@purplecallio/sdk';
const meeting = new PurpleCallioMeeting({
token, // bj_session_... for this participant
callId,
signalUrl: 'wss://api.purplecallio.com',
});
await meeting.join();
meeting.camera.enable();
meeting.microphone.disable();
await meeting.screenShare.start();
meeting.on('participant.joined', (p) => console.log('joined', p));
meeting.on('remote.stream', (stream) => attachToVideo(stream));
await meeting.leave();meeting.join()→ Connects socket, authenticates, joins room
meeting.leave()→ Ends the meeting, cleans up media
meeting.camera.enable() / disable()→ Toggle camera track
meeting.microphone.enable() / disable()→ Toggle microphone track
meeting.screenShare.start() / stop()→ Start / stop screen share
meeting.participants()→ Live list of participants
meeting.connectionState()→ Current connection state
meeting.on(event, cb)→ Subscribe to meeting events
Events
meeting.on('connected', (p) => {});
meeting.on('disconnected', () => {});
meeting.on('reconnected', () => {});
meeting.on('call.started', (d) => {});
meeting.on('call.ended', (d) => {});
meeting.on('participant.joined', (p) => {});
meeting.on('participant.left', (p) => {});
meeting.on('participant.updated', (p) => {});
meeting.on('camera.enabled' | 'camera.disabled', () => {});
meeting.on('microphone.enabled' | 'microphone.disabled', () => {});
meeting.on('screenShare.started' | 'screenShare.stopped', () => {});
meeting.on('remote.stream', (stream) => {});
meeting.on('remote.stream.ended', () => {});Angular SDK
An injectable service and video directive for Angular apps, built on the same engine as the JavaScript SDK. Requires Angular 16+.
npm install @purplecallio/angular @purplecallio/sdk
Quick example
import { Component, OnDestroy, OnInit } from '@angular/core';
import { PurpleCallioService } from '@purplecallio/angular';
@Component({
selector: 'app-call',
standalone: true,
template: `
<video purplecallioVideo [stream]="call.localStream$ | async" [muted]="true"></video>
<video purplecallioVideo [stream]="call.remoteStream$ | async"></video>
`,
})
export class CallComponent implements OnInit, OnDestroy {
constructor(public call: PurpleCallioService) {}
ngOnInit() {
this.call.configure({ token, callId, signalUrl });
this.call.join();
}
ngOnDestroy() {
this.call.leave();
}
}PurpleCallioServiceInjectable, providedIn root — configure(), join(), leave()
connectionState$ / participants$RxJS observables for live meeting state
remoteStream$ / localStream$Observables carrying MediaStream | null
camera / microphoneenable() / disable() / toggle() / isEnabled()
screenSharestart() / stop() / isActive()
purplecallioVideo directiveBinds a stream to a <video> element
React Native SDK
Reuses the same call engine as the web SDKs on top of react-native-webrtc, with permission handling, camera switching, and background/foreground lifecycle built in.
npm install @purplecallio/react-native @purplecallio/sdk react-native-webrtc
Quick example
import { registerGlobals } from 'react-native-webrtc';
registerGlobals(); // once, at app startup
import { PurpleCallioProvider, useMeeting, PurpleCallioVideoView } from '@purplecallio/react-native';
function CallScreen() {
const { join, remoteStream, localStream, toggleCamera, switchCamera } = useMeeting();
useEffect(() => { join(); }, [join]);
return <PurpleCallioVideoView stream={remoteStream} objectFit="cover" />;
}
export default function App() {
return (
<PurpleCallioProvider token={token} callId={callId} signalUrl={signalUrl}>
<CallScreen />
</PurpleCallioProvider>
);
}PurpleCallioProviderRequests camera/mic permissions before join(), manages lifecycle
useMeeting / useParticipantsSame hook names as @purplecallio/react
PurpleCallioVideoViewWraps react-native-webrtc's RTCView
switchCamera()Toggle between front and back camera
setSpeakerphoneOn()Requires the optional react-native-incall-manager package
pauseVideoInBackgroundCamera auto-pauses while the app is backgrounded (default on)
screenShare.start() rejects with a clear error.Vue SDK
A Composition API composable for Vue 3 apps, exposing connection state, participants, and media as reactive refs.
npm install @purplecallio/vue @purplecallio/sdk
Quick example
<script setup lang="ts">
import { usePurpleCallio } from '@purplecallio/vue';
import PurpleCallioVideo from '@purplecallio/vue/components/PurpleCallioVideo.vue';
const { join, leave, localStream, remoteStream, camera } = usePurpleCallio({
token, callId, signalUrl,
});
join();
</script>
<template>
<PurpleCallioVideo :stream="localStream" muted />
<PurpleCallioVideo :stream="remoteStream" />
<button @click="camera.toggle()">Toggle camera</button>
</template>usePurpleCallio(config)Composable — creates the engine and wires reactive state
connectionState / participantsReactive refs, usable directly in templates
remoteStream / localStreamshallowRef<MediaStream | null>
camera / microphoneenable() / disable() / toggle() / isEnabled()
screenSharestart() / stop() / isActive()
PurpleCallioVideoBinds a stream to a <video> element
usePurpleCallio() unmounts while still connected, it automatically leaves the call as a safety net — but call leave() explicitly when you're done.Svelte SDK
A factory function returning Svelte stores for connection state, participants, and media. Works with Svelte 4 and Svelte 5 (uses classic stores, not runes, so it works as a plain library module).
npm install @purplecallio/svelte @purplecallio/sdk
Quick example
<script lang="ts">
import { onDestroy } from 'svelte';
import { createPurpleCallio, PurpleCallioVideo } from '@purplecallio/svelte';
const call = createPurpleCallio({ token, callId, signalUrl });
call.join();
onDestroy(() => call.destroy());
</script>
<PurpleCallioVideo stream={$call.localStream} muted={true} />
<PurpleCallioVideo stream={$call.remoteStream} />
<button on:click={() => call.camera.toggle()}>Toggle camera</button>createPurpleCallio(config)Factory — creates the engine and returns stores + controls
connectionState / participantsReadable stores — use $ auto-subscription in .svelte files
remoteStream / localStreamReadable<MediaStream | null>
camera / microphoneenable() / disable() / toggle() / isEnabled()
screenSharestart() / stop() / isActive()
call.destroy()Unsubscribes and leaves — call from onDestroy()
createPurpleCallio() is not tied to any component lifecycle automatically — you must call call.destroy() yourself from onDestroy().REST API
Base URL: https://api.purplecallio.com
WebSocket Events
The hosted call UI connects automatically. Build a custom client? Here's the full reference.
import { io } from 'socket.io-client';
const socket = io('https://api.purplecallio.com', {
auth: { token: 'bj_session_...' },
transports: ['websocket'],
});
socket.on('connect', () => {
socket.emit('authenticate', { token: 'bj_session_...' });
});Connection
connected→ clientAuthenticated and joined the meeting. Carries your participant id.
{ participantId: 'user_alice' }disconnected→ clientSocket dropped.
{}reconnected→ clientSocket re-established.
{}Call + participant events
call.started→ bothCall became active.
{ callId: 'clx8f2z...' }call.ended→ bothCall ended by either side.
{ callId: 'clx8f2z...' }participant.joined→ othersA participant joined the room.
{ participantId: 'user_bob' }participant.left→ othersA participant left the room.
{ participantId: 'user_bob' }participant.updated→ othersParticipant media state changed.
{ participantId: 'user_bob', camera: false, microphone: true }incoming-call→ receiverLegacy alias — caller is waiting.
{ callId, callerId, type: 'VIDEO' }Media events
camera.enabled / camera.disabled→ othersCamera toggled.
{ callId }microphone.enabled / microphone.disabled→ othersMicrophone toggled.
{ callId }screenShare.started / screenShare.stopped→ othersScreen share toggled.
{ callId }WebRTC signaling
offer / answer / ice-candidate↔ bothWebRTC signaling events relayed by the server.
{ callId, offer? / answer? / candidate? }Webhooks
PurpleCallio POSTs a signed event to your server on every call lifecycle change.
Setup — Dashboard
Verify the signature
X-PurpleCallio-Signature header before processing. Use express.raw() — do not parse JSON first.import crypto from 'crypto';
import express from 'express';
app.post('/webhooks/purplecallio',
express.raw({ type: 'application/json' }),
(req, res) => {
const sig = req.headers['x-purplecallio-signature'];
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.PURPLECALLIO_WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return res.status(401).send('invalid signature');
}
const event = JSON.parse(req.body);
// event.event → 'call.created' | 'call.accepted' | ...
res.json({ ok: true });
}
);Examples
Hosted UI — create & redirect (Node.js)
import PurpleCallio from '@purplecallio/sdk';
const bj = new PurpleCallio({
apiKey: process.env.PURPLECALLIO_API_KEY,
baseUrl: 'https://api.purplecallio.com',
});
// 1. Create a call from your backend
const { callId, hostedUrl, participants } = await bj.createCall({
callerId: 'user_alice',
receiverId: 'user_bob',
type: 'VIDEO',
});
// 2. Redirect each participant to their hosted page
// participants[0].hostedUrl → Alice
// participants[1].hostedUrl → Bob
res.redirect(participants[0].hostedUrl);React Components
import { MeetingProvider, MeetingRoom, ParticipantGrid, CameraButton, MicrophoneButton, ScreenShareButton, LeaveButton, DeviceSelector } from '@purplecallio/react';
export function CustomCall({ token, callId, signalUrl }) {
return (
<MeetingProvider token={token} callId={callId} signalUrl={signalUrl}>
<MeetingRoom>
<ParticipantGrid />
</MeetingRoom>
<div style={{ display: 'flex', gap: 12, justifyContent: 'center', padding: 16 }}>
<CameraButton />
<MicrophoneButton />
<ScreenShareButton />
<DeviceSelector />
<LeaveButton />
</div>
</MeetingProvider>
);
}Headless SDK
import { PurpleCallioMeeting } from '@purplecallio/sdk';
const meeting = new PurpleCallioMeeting({ token, callId, signalUrl });
meeting.on('remote.stream', (stream) => {
document.getElementById('remote-video').srcObject = stream;
});
await meeting.join();
meeting.camera.enable();
meeting.microphone.enable();Error Codes
| status | meaning | fix |
|---|---|---|
| 400 | Bad Request | Check request body — missing required field. |
| 401 | Unauthorized | API key or session token is missing, invalid, or expired. |
| 403 | Forbidden | You do not own this resource. |
| 404 | Not Found | callId does not exist or belongs to another project. |
| 409 | Conflict | Call is already ENDED or REJECTED. |
| 429 | Rate Limited | Slow down — too many requests per second. |
| 500 | Server Error | Temporary. Retry with backoff. Contact support if persistent. |
Usage & Billing
PurpleCallio is pay-as-you-go. There are no subscriptions and no up-front fees — you pay a simple per-participant-minute rate only for usage beyond the monthly free allowance.
Audio
Loading…
/ participant-minute
Current allowance loading
Video
Loading…
/ participant-minute
Current allowance loading
Screen share
Loading…
/ participant-minute
Separate usage category; no free allowance
How billing works
Start free
Current free allowances are loaded from the billing service.
Add a payment method
In the dashboard, add a card only when you go to production. You are only charged for minutes beyond the free tier.
Monthly invoice
Current tax information is loaded from the billing service.
Failed payment
We retry and enter a 7-day grace period. Active calls are never interrupted, but new calls are blocked until payment succeeds.
❓ FAQ
Which integration should I pick?
Hosted UI for the fastest path (5 minutes). React Components for a branded custom interface without building WebRTC. Headless SDK if you need complete control over the UI.
Can I switch between the three products later?
Yes. All three use the same backend, the same POST /calls response, and the same session tokens. Change the frontend, keep your server-side integration.
Where do I put the API key?
Server-side only (bj_live_...). Never send it to the browser. The hosted page uses per-participant session tokens (bj_session_...), not API keys.
What about calls behind strict firewalls?
PurpleCallio provides TURN relay with time-limited credentials. The hosted UI and SDK fetch ICE servers automatically — no configuration needed.
Are session tokens single-use?
Yes. Each token is tied to one participant and one call. Create a new call if you need to re-invite someone.
Do you support group calls?
Currently 1:1 calls. Group calls are on the roadmap.
How do I debug a failed call?
Check the WebSocket connection state, verify the session token matches the correct participant, ensure camera/microphone permissions are granted, and confirm TURN credentials are returned from /turn/credentials.
Still stuck?
Try the playground — make a call in your browser with no code, no API key required.