Pinecall

Security

Token security model and best practices for production deployments.

Token security model#

Browser connections (WebRTC and chat) use short-lived tokens generated by the voice server. The recommended model:

Your backend generates tokens using your API key, and distributes them to browsers through your own auth layer.

This is the same model used by LiveKit, Twilio, Daily.co, and every major real-time platform.

Browser → Your Backend (your auth: session, JWT, OAuth)

         pc.createToken("webrtc", "florencia")
              ↓  (API key in Authorization header)
         voice.pinecall.io → { token, server, expiresIn }

         Your Backend returns token to browser

         Browser connects to voice.pinecall.io with token

Backend (Express, Next.js, Hono, etc.)#

import { Pinecall } from "@pinecall/sdk";

const pc = new Pinecall({ apiKey: process.env.PINECALL_API_KEY! });
await pc.connect();

const agent = pc.agent("florencia", { /* config */ });

// Token endpoint — protected by YOUR auth
app.get("/api/token", authMiddleware, async (req, res) => {
  const channel = req.query.channel as "webrtc" | "chat";
  const token = await agent.createToken(channel);
  res.json(token);
});

If the agent is in a separate process (you only have pc in the web server):

app.get("/api/token", authMiddleware, async (req, res) => {
  const token = await pc.createToken("webrtc", "florencia");
  res.json(token);
});

Frontend (VoiceWidget)#

<VoiceWidget
  agent="florencia"
  tokenProvider={async () => {
    const res = await fetch("/api/token?channel=webrtc", {
      credentials: "include", // send your session cookie
    });
    return res.json();
  }}
/>

Why tokens are safe#

Tokens have three security properties that make them safe to pass to browsers:

PropertyValueEffect
Single-useConsumed on first connectionCan't be reused by an attacker
Short-lived60 second TTLExpires before anyone can steal it
ScopedLocked to agent + orgCan't be used for a different agent

The token is not the security boundary — your backend is. The token is a short-lived capability that proves "someone authorized gave me permission to connect." The security question is: who can call your /api/token endpoint?

  • Requires login → only authenticated users get tokens
  • Rate limited → can't bulk-generate tokens
  • Permission-checked → only authorized users connect

Think of it like a movie ticket: the theater (your backend) verifies your identity and gives you a ticket. The ticket works once, for one screen, for a limited time. Even if someone steals the ticket, they get one session — and they'd need to break TLS to intercept it.

allowedOrigins (convenience mode)#

For simple deployments without a backend (demos, prototypes, CodePen), you can opt-in to public token access by configuring allowedOrigins:

const agent = pc.agent("demo-bot", {
  allowedOrigins: [
    "https://demo.mysite.com",      // exact match
    "https://*.mysite.com",          // subdomain wildcard
    "http://localhost:*",            // any port (dev)
  ],
});

When allowedOrigins is set, the token endpoint accepts browser requests from matching origins without an API key. The Origin header is browser-enforced (real browsers can't spoof it).

<VoiceWidget agent="demo-bot" apiKey="pk_publishable_..." />

Warning: allowedOrigins protects against casual embedding but not against a determined attacker — Origin headers can be spoofed from scripts/curl. For production with real users, always use tokenProvider with your backend auth.

Mode comparison#

ModeSecurity levelUse case
tokenProvider (backend)✅ Full auth controlProduction apps
allowedOrigins (public)⚠️ Origin-based onlyDemos, prototypes
Neither (default)❌ Rejected

API key handling#

Your PINECALL_API_KEY is the master credential. Treat it like a database password:

  • Never ship it in browser code, mobile apps, or public repos
  • Always load it from environment variables on the server
  • Rotate it if you suspect exposure (via the dashboard)
  • Scope it per-environment (separate keys for dev, staging, prod)

The SDK never exposes the API key over the wire in browser-bound responses — createToken returns only the short-lived token.

Webhook signature verification (WhatsApp)#

For WhatsApp, set appSecret so the server verifies the HMAC signature on every incoming webhook:

agent.addChannel("whatsapp", {
  phoneNumberId: "...",
  accessToken: "...",
  verifyToken: "...",
  appSecret: process.env.WA_APP_SECRET, // HMAC verification
});

Without appSecret, the webhook accepts any request — anyone who finds your endpoint URL can inject fake messages. Always set it in production.

What's next#