PurpleCallio
Start here

First call in 5 minutes

Two API calls. Two URLs. That's the entire integration.

01

Get your API key

Sign up → create a project → copy your key (starts with bj_live_).

02

Install the SDK

npm install @purplecallio/sdk
03

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',
});
04

Redirect each user — done

Alice opens callerUrl, Bob opens receiverUrl. PurpleCallio handles the rest.

Your bj_live_ API key is server-side only. Never send it to the browser.

How it works

Your backend

POST /calls

PurpleCallio

Returns 2 URLs

Alice (caller)

Opens callerUrl

Bob (receiver)

Opens receiverUrl

Signaling — WebSocket events between browser and server
WebRTC — peer-to-peer media, negotiated automatically
TURN — relay for calls behind strict firewalls

Three types of credentials

bj_live_...

API Key

Your server → REST API

bj_session_...

Session Token

Browser → WebSocket

JWT Bearer

Dashboard JWT

Dashboard UI → management API

Authentication

Every REST request needs your API key in the x-api-key header.

Header
x-api-key: $PURPLECALLIO_API_KEY
Session tokens in 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.

Ready-made meeting UI
Video & audio calling
Screen sharing
Device selection
Waiting room
Responsive design
Branding support
WebRTC + TURN handled

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.

Project settings (dashboard)
{
  "branding": {
    "companyName": "Acme",
    "logoUrl": "https://cdn.acme.com/logo.png",
    "primaryColor": "#2563EB"
  },
  "theme": "dark",
  "waitingRoom": true
}
Integration time: 5 minutes. Create a call, redirect your users, done.

⚛️ React UI Components

Build a custom interface with reusable React components — no need to implement WebRTC, signaling, or media handling yourself.

install
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>
  );
}
MeetingProvider

Context provider — wires the engine, media, and signaling

MeetingRoom

Meeting layout shell with waiting room support

ParticipantGrid / ParticipantTile

Grid of participant video tiles

ActiveSpeakerView

Large speaker view + local PiP

CameraButton / MicrophoneButton

Toggle camera / microphone

ScreenShareButton / LeaveButton

Screen share + end call controls

DeviceSelector

Camera / microphone / speaker picker

WaitingRoom

Waiting room panel

ConnectionStatus

Live connection state indicator

SpeakingIndicator

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

install
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+.

install
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();
  }
}
PurpleCallioService

Injectable, providedIn root — configure(), join(), leave()

connectionState$ / participants$

RxJS observables for live meeting state

remoteStream$ / localStream$

Observables carrying MediaStream | null

camera / microphone

enable() / disable() / toggle() / isEnabled()

screenShare

start() / stop() / isActive()

purplecallioVideo directive

Binds a stream to a <video> element

The service does not expose the raw engine instance — state is only available through observables and the control methods above, keeping the public surface small.

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.

install
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>
  );
}
PurpleCallioProvider

Requests camera/mic permissions before join(), manages lifecycle

useMeeting / useParticipants

Same hook names as @purplecallio/react

PurpleCallioVideoView

Wraps react-native-webrtc's RTCView

switchCamera()

Toggle between front and back camera

setSpeakerphoneOn()

Requires the optional react-native-incall-manager package

pauseVideoInBackground

Camera auto-pauses while the app is backgrounded (default on)

Screen sharing is not yet supported on React Native — it requires native platform integration (an iOS Broadcast Upload Extension and Android MediaProjection) not yet implemented in this package. Calling 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.

install
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 / participants

Reactive refs, usable directly in templates

remoteStream / localStream

shallowRef<MediaStream | null>

camera / microphone

enable() / disable() / toggle() / isEnabled()

screenShare

start() / stop() / isActive()

PurpleCallioVideo

Binds a stream to a <video> element

If the component using 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).

install
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 / participants

Readable stores — use $ auto-subscription in .svelte files

remoteStream / localStream

Readable<MediaStream | null>

camera / microphone

enable() / disable() / toggle() / isEnabled()

screenShare

start() / stop() / isActive()

call.destroy()

Unsubscribes and leaves — call from onDestroy()

Unlike the React/Vue/Angular adapters, 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.

connect
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→ client

Authenticated and joined the meeting. Carries your participant id.

{ participantId: 'user_alice' }
disconnected→ client

Socket dropped.

{}
reconnected→ client

Socket re-established.

{}

Call + participant events

call.started→ both

Call became active.

{ callId: 'clx8f2z...' }
call.ended→ both

Call ended by either side.

{ callId: 'clx8f2z...' }
participant.joined→ others

A participant joined the room.

{ participantId: 'user_bob' }
participant.left→ others

A participant left the room.

{ participantId: 'user_bob' }
participant.updated→ others

Participant media state changed.

{ participantId: 'user_bob', camera: false, microphone: true }
incoming-call→ receiver

Legacy alias — caller is waiting.

{ callId, callerId, type: 'VIDEO' }

Media events

camera.enabled / camera.disabled→ others

Camera toggled.

{ callId }
microphone.enabled / microphone.disabled→ others

Microphone toggled.

{ callId }
screenShare.started / screenShare.stopped→ others

Screen share toggled.

{ callId }

WebRTC signaling

offer / answer / ice-candidate↔ both

WebRTC 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

Open projectWebhook sectionPaste your URLSecret auto-generated
call.created
call.accepted
call.rejected
call.ended

Verify the signature

Always verify the 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)

your-server.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

statusmeaningfix
400Bad RequestCheck request body — missing required field.
401UnauthorizedAPI key or session token is missing, invalid, or expired.
403ForbiddenYou do not own this resource.
404Not FoundcallId does not exist or belongs to another project.
409ConflictCall is already ENDED or REJECTED.
429Rate LimitedSlow down — too many requests per second.
500Server ErrorTemporary. 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.

Screen sharing is a separately tracked, billable usage category with no free allowance. Everything else — Hosted UI, React Components, Headless SDK, REST API, signaling, and the dashboard — is included on the free tier.

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