Voltar para Materiais
05 de Março de 202618 min de leitura

Como Integrar Videochamadas com Next.js, React ou Angular em 30 Minutos

Guia prático com código pronto para copiar e colar. Do iframe mais simples até a integração completa com gravação, transcrição e webhooks — escolha o nível certo para o seu projeto.

Next.jsReactAngularAPI RESTIframe

Introdução

Adicionar videochamadas ao seu app não precisa ser um projeto de meses. Se você trabalha com WebRTC diretamente, sabe que envolve servidores STUN/TURN, sinalização, gerenciamento de mídia, compatibilidade entre navegadores, reconexão automática e uma lista interminável de edge cases. Mas com uma API pronta, o processo se resume a uma chamada REST e um iframe.

Neste tutorial, vamos cobrir três abordagens para integrar videochamadas da videochamada.com.br no seu projeto, independente do framework que você usa:

5 MINUTOS

Iframe Simples

Crie a chamada via API, coloque a URL num iframe. Funciona em qualquer framework. Ideal para MVPs e ferramentas internas.

15 MINUTOS

API + Iframe

Backend proxy para proteger a API key, componente dedicado, ciclo de vida da chamada. A abordagem recomendada.

30 MINUTOS

Integração Completa

Tudo acima + gravação, transcrição, webhooks e personalização. Para apps de produção que precisam de controle total.

Cada seção inclui código completo e funcional para Next.js, React (Vite) e Angular. Você pode alternar entre os frameworks em cada exemplo usando as abas de código.

Pré-requisitos

Antes de começar, verifique se você tem tudo o que precisa. A configuração é mínima — não há SDKs para instalar ou bibliotecas de WebRTC para configurar.

Conta na videochamada.com.br

Crie uma conta gratuita em app.videochamada.com.br/cadastrar. Não precisa de cartão de crédito. Você recebe 2.000 minutos grátis por mês — suficiente para desenvolvimento, testes e até produção de projetos menores.

API Key

Após criar a conta, vá em Configurações > API Keys e gere sua chave. Você vai usá-la para autenticar todas as chamadas à API. Guarde-a em local seguro — ela dá acesso total à sua conta.

Projeto rodando em Next.js, React ou Angular

Qualquer projeto existente funciona. Se quiser começar do zero, use npx create-next-app@latest, npm create vite@latest ou ng new. A integração não depende de nenhuma biblioteca adicional — só HTTP requests e um iframe.

Documentação completa da API

Este tutorial cobre os endpoints essenciais. Para a referência completa com todos os parâmetros, status codes e exemplos, acesse documentacao.videochamada.com.br.

5 minQualquer framework

Método 1: Iframe Simples

A forma mais rápida de colocar videochamada no seu app. O fluxo é direto: crie uma chamada via API, pegue a URL retornada, e coloque num iframe. Funciona em qualquer framework — e até em HTML puro.

Passo 1: Crie uma chamada via API

Use curl para testar no terminal antes de integrar no código. A API retorna um objeto com o id da chamada e a url para acessá-la:

TerminalcURL
curl -X POST https://api.videochamada.com.br/calls \
  -H "Authorization: Bearer SUA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "participantName": "Dr. Silva",
    "participantRole": "host"
  }'

# Resposta:
# {
#   "id": "call-a1b2c3d4-e5f6-7890-abcd-ef1234567890",
#   "url": "https://call.videochamada.com.br/sala-abc123",
#   "status": "waiting",
#   "createdAt": "2026-03-05T14:30:00.000Z"
# }

O mesmo request via fetch no JavaScript:

createCall.jsJavaScript
const response = await fetch('https://api.videochamada.com.br/calls', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${API_KEY}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    participantName: 'Dr. Silva',
    participantRole: 'host'
  })
});

const call = await response.json();
console.log(call.url); // https://call.videochamada.com.br/sala-abc123

Passo 2: Exiba no iframe

Com a URL da chamada em mãos, basta renderizar um iframe. Aqui vai o código para cada framework:

components/VideoCall.tsxNext.js
// components/VideoCall.tsx
import { useState } from 'react';

interface VideoCallProps {
  callUrl: string;
  onClose: () => void;
}

export default function VideoCall({ callUrl, onClose }: VideoCallProps) {
  const [isLoading, setIsLoading] = useState(true);

  return (
    <div style={{
      position: 'fixed',
      inset: 0,
      zIndex: 50,
      backgroundColor: 'rgba(0,0,0,0.95)',
      display: 'flex',
      flexDirection: 'column'
    }}>
      <div style={{
        display: 'flex',
        justifyContent: 'space-between',
        alignItems: 'center',
        padding: '12px 16px',
        backgroundColor: '#111'
      }}>
        <span style={{ color: '#fff', fontWeight: 600 }}>
          Videochamada em andamento
        </span>
        <button
          onClick={onClose}
          style={{
            backgroundColor: '#ef4444',
            color: '#fff',
            border: 'none',
            padding: '8px 16px',
            borderRadius: '6px',
            cursor: 'pointer',
            fontWeight: 600
          }}
        >
          Encerrar
        </button>
      </div>

      {isLoading && (
        <div style={{
          position: 'absolute',
          inset: 0,
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
          color: '#fff'
        }}>
          Carregando videochamada...
        </div>
      )}

      <iframe
        src={callUrl}
        onLoad={() => setIsLoading(false)}
        allow="camera; microphone; display-capture"
        style={{
          flex: 1,
          width: '100%',
          border: 'none'
        }}
      />
    </div>
  );
}

Atenção: API Key no frontend

No exemplo do fetch acima, a API Key está no cliente. Isso funciona para testes rápidos, mas nunca faça isso em produção. Qualquer usuário pode inspecionar o código e roubar sua chave. Use um backend proxy — é o que vamos cobrir nos próximos métodos.

Quando usar este método?

  • MVPs e protótipos onde velocidade é prioridade
  • Ferramentas internas onde a API key pode ficar em variável de ambiente no server
  • Provas de conceito para mostrar ao cliente
  • Ambientes de staging/teste controlados
15 minNext.js

Método 2: Integração com Next.js

O Next.js é o framework ideal para essa integração porque oferece API Routes nativamente — você não precisa de um backend separado. A API Key fica segura no servidor, e o frontend só recebe a URL da chamada.

Passo 1: Configure as variáveis de ambiente

.env.localNext.js
# .env.local (NUNCA commite este arquivo!)
VIDEOCHAMADA_API_KEY=sua_api_key_aqui
NEXT_PUBLIC_APP_URL=http://localhost:3000

Passo 2: Crie a API Route

Esta rota funciona como proxy seguro. O frontend chama /api/calls do seu domínio, e o servidor repassa para a API da videochamada.com.br com a chave de autenticação. A API Key nunca chega ao navegador.

pages/api/calls.tsNext.js API Route
// pages/api/calls.ts
import type { NextApiRequest, NextApiResponse } from 'next';

interface CreateCallBody {
  participantName: string;
  participantRole?: 'host' | 'participant';
  recording?: boolean;
  transcription?: boolean;
}

interface CallResponse {
  id: string;
  url: string;
  status: string;
  createdAt: string;
}

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' });
  }

  const apiKey = process.env.VIDEOCHAMADA_API_KEY;
  if (!apiKey) {
    console.error('VIDEOCHAMADA_API_KEY not configured');
    return res.status(500).json({ error: 'Server configuration error' });
  }

  try {
    const body: CreateCallBody = req.body;

    const response = await fetch('https://api.videochamada.com.br/calls', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${apiKey}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        participantName: body.participantName,
        participantRole: body.participantRole || 'host',
        recording: body.recording || false,
        transcription: body.transcription || false
      })
    });

    if (!response.ok) {
      const error = await response.json();
      return res.status(response.status).json(error);
    }

    const data: CallResponse = await response.json();
    return res.status(200).json(data);
  } catch (error) {
    console.error('Error creating call:', error);
    return res.status(500).json({ error: 'Failed to create call' });
  }
}

Passo 3: Crie o componente de videochamada

O componente gerencia o ciclo de vida completo: criar a chamada, exibir o iframe, e limpar ao encerrar. Usamos dynamic import para garantir que o componente só renderize no cliente (o iframe precisa do DOM do navegador).

components/VideoCallManager.tsxNext.js
// components/VideoCallManager.tsx
import { useState, useCallback } from 'react';

type CallStatus = 'idle' | 'creating' | 'active' | 'error';

interface CallData {
  id: string;
  url: string;
}

export default function VideoCallManager() {
  const [status, setStatus] = useState<CallStatus>('idle');
  const [callData, setCallData] = useState<CallData | null>(null);
  const [error, setError] = useState<string | null>(null);

  const createCall = useCallback(async () => {
    setStatus('creating');
    setError(null);

    try {
      const response = await fetch('/api/calls', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          participantName: 'Usuário',
          participantRole: 'host'
        })
      });

      if (!response.ok) {
        throw new Error('Erro ao criar chamada');
      }

      const data = await response.json();
      setCallData(data);
      setStatus('active');
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Erro desconhecido');
      setStatus('error');
    }
  }, []);

  const endCall = useCallback(() => {
    setCallData(null);
    setStatus('idle');
  }, []);

  if (status === 'active' && callData) {
    return (
      <div style={{
        position: 'fixed', inset: 0, zIndex: 50,
        background: 'rgba(0,0,0,0.95)',
        display: 'flex', flexDirection: 'column'
      }}>
        <div style={{
          display: 'flex', justifyContent: 'space-between',
          alignItems: 'center', padding: '12px 16px',
          background: '#111'
        }}>
          <span style={{ color: '#fff', fontWeight: 600 }}>
            Chamada: {callData.id.slice(0, 8)}...
          </span>
          <button
            onClick={endCall}
            style={{
              background: '#ef4444', color: '#fff',
              border: 'none', padding: '8px 16px',
              borderRadius: '6px', cursor: 'pointer',
              fontWeight: 600
            }}
          >
            Encerrar Chamada
          </button>
        </div>
        <iframe
          src={callData.url}
          allow="camera; microphone; display-capture"
          style={{ flex: 1, width: '100%', border: 'none' }}
        />
      </div>
    );
  }

  return (
    <div style={{ padding: '24px' }}>
      <button
        onClick={createCall}
        disabled={status === 'creating'}
        style={{
          background: status === 'creating' ? '#666' : '#2563eb',
          color: '#fff', border: 'none',
          padding: '12px 24px', borderRadius: '8px',
          cursor: status === 'creating' ? 'wait' : 'pointer',
          fontWeight: 600, fontSize: '16px'
        }}
      >
        {status === 'creating' ? 'Criando chamada...' : 'Iniciar Videochamada'}
      </button>

      {error && (
        <p style={{ color: '#ef4444', marginTop: '12px' }}>{error}</p>
      )}
    </div>
  );
}

Passo 4: Use na página com dynamic import

pages/chamada.tsxNext.js
// pages/chamada.tsx
import dynamic from 'next/dynamic';

// Dynamic import evita SSR — o iframe precisa do browser
const VideoCallManager = dynamic(
  () => import('../components/VideoCallManager'),
  { ssr: false, loading: () => <p>Carregando...</p> }
);

export default function ChamadaPage() {
  return (
    <div>
      <h1>Sala de Videochamada</h1>
      <VideoCallManager />
    </div>
  );
}

Dicas para Next.js

  • Use dynamic import com ssr: false para componentes que usam iframe — evita erros de hydration
  • A API Route roda no servidor, então a API Key nunca é exposta ao cliente
  • No App Router (Next.js 13+), use app/api/calls/route.ts com NextResponse ao invés de pages/api
  • Para o App Router, adicione 'use client' no componente de vídeo
15 minReact (Vite)

Método 3: Integração com React (Vite)

Se o seu projeto React roda no Vite (ou CRA), você não tem API routes nativas como no Next.js. Você precisa de um backend separado para proteger a API Key. Vamos usar um servidor Express mínimo como proxy, mas você pode substituir por Supabase Edge Functions, Cloudflare Workers, ou qualquer backend que já tenha.

Passo 1: Crie o backend proxy

Um servidor Express com uma única rota. Pode rodar separado ou no mesmo repositório:

server/index.tsExpress (Backend Proxy)
// server/index.ts
import express from 'express';
import cors from 'cors';

const app = express();
app.use(cors({ origin: 'http://localhost:5173' })); // URL do Vite
app.use(express.json());

const API_KEY = process.env.VIDEOCHAMADA_API_KEY;

app.post('/api/calls', async (req, res) => {
  try {
    const response = await fetch('https://api.videochamada.com.br/calls', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${API_KEY}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        participantName: req.body.participantName,
        participantRole: req.body.participantRole || 'host',
        recording: req.body.recording || false,
        transcription: req.body.transcription || false
      })
    });

    const data = await response.json();

    if (!response.ok) {
      return res.status(response.status).json(data);
    }

    res.json(data);
  } catch (error) {
    console.error('Proxy error:', error);
    res.status(500).json({ error: 'Failed to create call' });
  }
});

app.listen(3001, () => {
  console.log('Proxy server running on http://localhost:3001');
});

Alternativa sem Express: Se você já usa Supabase, crie uma Edge Function (como no guia do Lovable). Se usa Vercel, crie uma api/calls.ts Serverless Function. O importante é que a API Key fique no servidor.

Passo 2: Configure o frontend

.envReact (Vite)
# .env (frontend - SEM api key aqui!)
VITE_API_URL=http://localhost:3001

Passo 3: Crie o hook useVideoCall

Encapsulamos toda a lógica de criação e gerenciamento da chamada num hook customizado. Isso mantém os componentes limpos e permite reutilizar a lógica em diferentes páginas.

src/hooks/useVideoCall.tsReact (Vite)
// src/hooks/useVideoCall.ts
import { useState, useCallback } from 'react';

type CallStatus = 'idle' | 'creating' | 'active' | 'ended' | 'error';

interface CallData {
  id: string;
  url: string;
  status: string;
}

interface UseVideoCallOptions {
  participantName: string;
  participantRole?: 'host' | 'participant';
  recording?: boolean;
  transcription?: boolean;
}

export function useVideoCall() {
  const [status, setStatus] = useState<CallStatus>('idle');
  const [callData, setCallData] = useState<CallData | null>(null);
  const [error, setError] = useState<string | null>(null);

  const apiUrl = import.meta.env.VITE_API_URL;

  const createCall = useCallback(async (options: UseVideoCallOptions) => {
    setStatus('creating');
    setError(null);

    try {
      const response = await fetch(`${apiUrl}/api/calls`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(options)
      });

      if (!response.ok) {
        const err = await response.json();
        throw new Error(err.error || 'Erro ao criar chamada');
      }

      const data: CallData = await response.json();
      setCallData(data);
      setStatus('active');
      return data;
    } catch (err) {
      const message = err instanceof Error ? err.message : 'Erro desconhecido';
      setError(message);
      setStatus('error');
      return null;
    }
  }, [apiUrl]);

  const endCall = useCallback(() => {
    setCallData(null);
    setStatus('ended');
  }, []);

  const reset = useCallback(() => {
    setCallData(null);
    setStatus('idle');
    setError(null);
  }, []);

  return {
    status,
    callData,
    error,
    createCall,
    endCall,
    reset,
    isActive: status === 'active',
    isCreating: status === 'creating'
  };
}

Passo 4: Crie o componente

src/components/VideoCallRoom.tsxReact (Vite)
// src/components/VideoCallRoom.tsx
import { useEffect } from 'react';
import { useVideoCall } from '../hooks/useVideoCall';

export function VideoCallRoom() {
  const {
    status, callData, error,
    createCall, endCall, reset, isCreating
  } = useVideoCall();

  // Cleanup: se o componente desmontar durante uma chamada, encerra
  useEffect(() => {
    return () => {
      // Cleanup ao desmontar o componente
    };
  }, []);

  if (status === 'active' && callData) {
    return (
      <div style={{
        position: 'fixed', inset: 0, zIndex: 50,
        background: '#000',
        display: 'flex', flexDirection: 'column'
      }}>
        <div style={{
          display: 'flex', justifyContent: 'space-between',
          alignItems: 'center', padding: '12px 16px',
          background: '#111', borderBottom: '1px solid #333'
        }}>
          <div>
            <span style={{ color: '#fff', fontWeight: 600 }}>
              Chamada ativa
            </span>
            <span style={{
              marginLeft: '12px', color: '#4ade80',
              fontSize: '14px'
            }}>
              ID: {callData.id.slice(0, 8)}
            </span>
          </div>
          <button
            onClick={endCall}
            style={{
              background: '#ef4444', color: '#fff',
              border: 'none', padding: '8px 20px',
              borderRadius: '6px', cursor: 'pointer',
              fontWeight: 600
            }}
          >
            Encerrar
          </button>
        </div>
        <iframe
          src={callData.url}
          allow="camera; microphone; display-capture"
          style={{ flex: 1, width: '100%', border: 'none' }}
        />
      </div>
    );
  }

  return (
    <div style={{ padding: '32px', maxWidth: '400px' }}>
      <h2 style={{ marginBottom: '16px', fontSize: '20px' }}>
        Videochamada
      </h2>

      <button
        onClick={() => createCall({
          participantName: 'Usuário',
          participantRole: 'host'
        })}
        disabled={isCreating}
        style={{
          width: '100%',
          background: isCreating ? '#555' : '#2563eb',
          color: '#fff', border: 'none',
          padding: '14px 24px', borderRadius: '8px',
          cursor: isCreating ? 'wait' : 'pointer',
          fontWeight: 600, fontSize: '16px'
        }}
      >
        {isCreating ? 'Criando chamada...' : 'Iniciar Videochamada'}
      </button>

      {error && (
        <div style={{
          marginTop: '12px', padding: '12px',
          background: '#fef2f2', borderRadius: '6px',
          color: '#dc2626', fontSize: '14px'
        }}>
          {error}
          <button
            onClick={reset}
            style={{
              marginLeft: '8px', textDecoration: 'underline',
              background: 'none', border: 'none',
              color: '#dc2626', cursor: 'pointer'
            }}
          >
            Tentar novamente
          </button>
        </div>
      )}

      {status === 'ended' && (
        <div style={{
          marginTop: '12px', padding: '12px',
          background: '#f0fdf4', borderRadius: '6px',
          color: '#16a34a', fontSize: '14px'
        }}>
          Chamada encerrada com sucesso.
          <button
            onClick={reset}
            style={{
              marginLeft: '8px', textDecoration: 'underline',
              background: 'none', border: 'none',
              color: '#16a34a', cursor: 'pointer'
            }}
          >
            Nova chamada
          </button>
        </div>
      )}
    </div>
  );
}

Dicas para React (Vite)

  • O hook useVideoCall encapsula toda a lógica — os componentes ficam focados em UI
  • Use o useEffect cleanup para lidar com unmount durante uma chamada ativa
  • Variáveis de ambiente no Vite usam o prefixo VITE_ — sem ele, a variável não é exposta ao frontend
  • Configure o proxy do Vite em vite.config.ts para evitar CORS em desenvolvimento
15 minAngular

Método 4: Integração com Angular

No Angular, a integração segue o padrão de services e components que você já conhece. Criamos um VideoCallService para a comunicação com a API e um VideoCallComponent para renderizar o iframe. Assim como no React, você precisa de um backend proxy — usamos o mesmo servidor Express ou qualquer backend que você já tenha.

Passo 1: Configure o environment

src/environments/environment.tsAngular
// src/environments/environment.ts
export const environment = {
  production: false,
  apiUrl: 'http://localhost:3001' // URL do seu backend proxy
};
src/environments/environment.prod.tsAngular
// src/environments/environment.prod.ts
export const environment = {
  production: true,
  apiUrl: 'https://api.seudominio.com.br' // URL de produção do backend
};

Passo 2: Crie o VideoCallService

src/app/services/video-call.service.tsAngular
// src/app/services/video-call.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, BehaviorSubject } from 'rxjs';
import { tap, catchError } from 'rxjs/operators';
import { environment } from '../../environments/environment';

export interface CallData {
  id: string;
  url: string;
  status: string;
  createdAt: string;
}

export interface CreateCallOptions {
  participantName: string;
  participantRole?: 'host' | 'participant';
  recording?: boolean;
  transcription?: boolean;
}

export type CallStatus = 'idle' | 'creating' | 'active' | 'ended' | 'error';

@Injectable({
  providedIn: 'root'
})
export class VideoCallService {
  private statusSubject = new BehaviorSubject<CallStatus>('idle');
  private callDataSubject = new BehaviorSubject<CallData | null>(null);
  private errorSubject = new BehaviorSubject<string | null>(null);

  status$ = this.statusSubject.asObservable();
  callData$ = this.callDataSubject.asObservable();
  error$ = this.errorSubject.asObservable();

  constructor(private http: HttpClient) {}

  createCall(options: CreateCallOptions): Observable<CallData> {
    this.statusSubject.next('creating');
    this.errorSubject.next(null);

    return this.http
      .post<CallData>(`${environment.apiUrl}/api/calls`, options)
      .pipe(
        tap((data) => {
          this.callDataSubject.next(data);
          this.statusSubject.next('active');
        }),
        catchError((err) => {
          this.errorSubject.next(
            err.error?.message || 'Erro ao criar chamada'
          );
          this.statusSubject.next('error');
          throw err;
        })
      );
  }

  endCall(): void {
    this.callDataSubject.next(null);
    this.statusSubject.next('ended');
  }

  reset(): void {
    this.callDataSubject.next(null);
    this.statusSubject.next('idle');
    this.errorSubject.next(null);
  }
}

Passo 3: Crie o VideoCallComponent

src/app/components/video-call/video-call.component.tsAngular
// src/app/components/video-call/video-call.component.ts
import { Component, OnDestroy } from '@angular/core';
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
import { Subscription } from 'rxjs';
import {
  VideoCallService,
  CallData,
  CallStatus
} from '../../services/video-call.service';

@Component({
  selector: 'app-video-call',
  template: `
    <!-- Botão para iniciar -->
    <div *ngIf="status !== 'active'" class="call-controls">
      <h2>Videochamada</h2>

      <button
        (click)="startCall()"
        [disabled]="status === 'creating'"
        class="start-btn"
        [class.loading]="status === 'creating'"
      >
        {{ status === 'creating' ? 'Criando chamada...' : 'Iniciar Videochamada' }}
      </button>

      <div *ngIf="error" class="error-msg">
        {{ error }}
        <button (click)="reset()" class="retry-btn">Tentar novamente</button>
      </div>

      <div *ngIf="status === 'ended'" class="success-msg">
        Chamada encerrada com sucesso.
        <button (click)="reset()" class="retry-btn">Nova chamada</button>
      </div>
    </div>

    <!-- Iframe da videochamada -->
    <div *ngIf="status === 'active' && safeUrl" class="call-overlay">
      <div class="call-toolbar">
        <span class="call-id">
          Chamada ativa
          <small *ngIf="callData">ID: {{ callData.id | slice:0:8 }}</small>
        </span>
        <button (click)="endCall()" class="end-btn">Encerrar</button>
      </div>
      <iframe
        [src]="safeUrl"
        allow="camera; microphone; display-capture"
        class="call-frame"
      ></iframe>
    </div>
  `,
  styles: [`
    .call-controls {
      padding: 32px; max-width: 400px;
    }
    .call-controls h2 {
      margin-bottom: 16px; font-size: 20px;
    }
    .start-btn {
      width: 100%; background: #2563eb; color: #fff;
      border: none; padding: 14px 24px; border-radius: 8px;
      cursor: pointer; font-weight: 600; font-size: 16px;
    }
    .start-btn.loading {
      background: #555; cursor: wait;
    }
    .error-msg {
      margin-top: 12px; padding: 12px; background: #fef2f2;
      border-radius: 6px; color: #dc2626; font-size: 14px;
    }
    .success-msg {
      margin-top: 12px; padding: 12px; background: #f0fdf4;
      border-radius: 6px; color: #16a34a; font-size: 14px;
    }
    .retry-btn {
      margin-left: 8px; text-decoration: underline;
      background: none; border: none; cursor: pointer;
      color: inherit;
    }
    .call-overlay {
      position: fixed; inset: 0; z-index: 50;
      background: #000; display: flex;
      flex-direction: column;
    }
    .call-toolbar {
      display: flex; justify-content: space-between;
      align-items: center; padding: 12px 16px;
      background: #111; border-bottom: 1px solid #333;
    }
    .call-id { color: #fff; font-weight: 600; }
    .call-id small {
      margin-left: 12px; color: #4ade80; font-size: 14px;
    }
    .end-btn {
      background: #ef4444; color: #fff; border: none;
      padding: 8px 20px; border-radius: 6px;
      cursor: pointer; font-weight: 600;
    }
    .call-frame {
      flex: 1; width: 100%; border: none;
    }
  `]
})
export class VideoCallComponent implements OnDestroy {
  status: CallStatus = 'idle';
  callData: CallData | null = null;
  error: string | null = null;
  safeUrl: SafeResourceUrl | null = null;

  private subscriptions = new Subscription();

  constructor(
    private videoCallService: VideoCallService,
    private sanitizer: DomSanitizer
  ) {
    this.subscriptions.add(
      this.videoCallService.status$.subscribe(s => this.status = s)
    );
    this.subscriptions.add(
      this.videoCallService.callData$.subscribe(data => {
        this.callData = data;
        this.safeUrl = data
          ? this.sanitizer.bypassSecurityTrustResourceUrl(data.url)
          : null;
      })
    );
    this.subscriptions.add(
      this.videoCallService.error$.subscribe(e => this.error = e)
    );
  }

  startCall(): void {
    this.videoCallService.createCall({
      participantName: 'Usuário',
      participantRole: 'host'
    }).subscribe();
  }

  endCall(): void {
    this.videoCallService.endCall();
  }

  reset(): void {
    this.videoCallService.reset();
  }

  ngOnDestroy(): void {
    this.subscriptions.unsubscribe();
  }
}

Passo 4: Registre no módulo

src/app/app.module.tsAngular
// src/app/app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { AppComponent } from './app.component';
import { VideoCallComponent } from './components/video-call/video-call.component';

@NgModule({
  declarations: [
    AppComponent,
    VideoCallComponent
  ],
  imports: [
    BrowserModule,
    HttpClientModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {}

// No template do AppComponent ou qualquer outro:
// <app-video-call></app-video-call>

Dicas para Angular

  • Use DomSanitizer.bypassSecurityTrustResourceUrl() para URLs no iframe — Angular bloqueia URLs dinâmicas por segurança
  • Sempre use ngOnDestroy para fazer unsubscribe dos Observables e evitar memory leaks
  • Use environment.ts para a URL da API — o Angular substitui automaticamente em build de produção
  • Para lazy loading, crie um VideoCallModule separado e carregue-o sob demanda na rota
30 minTodos os frameworks

Recursos Avançados

Com a integração básica funcionando, você pode ativar recursos avançados com poucas linhas de código. Todos os recursos abaixo são ativados via parâmetros na criação da chamada — não requerem mudanças no frontend.

Gravação

R$ 0,075/min de gravação

Ative a gravação passando recording: true na criação da chamada. A gravação começa automaticamente quando o primeiro participante entra e para quando o último sai. Você recebe o arquivo via webhook.

API RequestJSON
{
  "participantName": "Dr. Silva",
  "participantRole": "host",
  "recording": true
}

Transcrição

R$ 0,0225/min de transcrição

A transcrição usa IA para converter o áudio da chamada em texto. Ative com transcription: true. O resultado é entregue via webhook no formato JSON com timestamps por fala.

API RequestJSON
{
  "participantName": "Dr. Silva",
  "participantRole": "host",
  "recording": true,
  "transcription": true
}

Webhooks

Notificações em tempo real

Configure uma URL de webhook nas configurações da sua conta para receber eventos automaticamente. Os eventos disponíveis são:

call.started

Quando o primeiro participante entra na chamada

call.ended

Quando o último participante sai da chamada

recording.ready

Quando a gravação termina de processar

transcription.ready

Quando a transcrição está pronta

pages/api/webhooks/videochamada.tsNext.js
// pages/api/webhooks/videochamada.ts
import type { NextApiRequest, NextApiResponse } from 'next';

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method !== 'POST') {
    return res.status(405).end();
  }

  const event = req.body;

  switch (event.type) {
    case 'call.started':
      console.log('Chamada iniciada:', event.data.callId);
      // Atualize o status no seu banco de dados
      break;

    case 'call.ended':
      console.log('Chamada encerrada:', event.data.callId);
      console.log('Duração:', event.data.durationMinutes, 'min');
      // Salve a duração, atualize billing, etc.
      break;

    case 'recording.ready':
      console.log('Gravação pronta:', event.data.recordingUrl);
      // Salve a URL, notifique o usuário, faça download
      break;

    case 'transcription.ready':
      console.log('Transcrição pronta:', event.data.transcriptionUrl);
      // Salve o texto, indexe para busca, etc.
      break;
  }

  // Responda 200 para confirmar recebimento
  res.status(200).json({ received: true });
}

Personalização Whitelabel

Incluso em todos os planos

Personalize a interface de videochamada com as cores e logo da sua marca. A configuração é feita no painel da videochamada.com.br em Configurações > Whitelabel. As opções incluem:

  • Cor primária e secundária da interface
  • Logo da sua empresa na sala de chamada
  • Textos customizados (sala de espera, botões)
  • Background virtual padrão da empresa

Metadados do Participante

Dados adicionais na chamada

Passe informações adicionais sobre o participante que serão incluídas nos webhooks e relatórios. Útil para associar chamadas a usuários, agendamentos ou sessões no seu sistema.

API RequestJSON
{
  "participantName": "Dr. Silva",
  "participantRole": "host",
  "metadata": {
    "userId": "usr_12345",
    "appointmentId": "apt_67890",
    "specialty": "cardiologia",
    "source": "meu-app-saude"
  }
}

Segurança

A segurança da integração depende de como você trata a API Key e valida o acesso às chamadas. Siga estas práticas para evitar problemas:

NUNCA exponha a API Key no frontend

A API Key dá acesso total à sua conta: criar chamadas, acessar gravações, gerenciar configurações. Se um usuário mal-intencionado capturar sua chave via DevTools, pode usá-la para consumir seus minutos, acessar gravações de outros usuários, ou gerar custos na sua conta.

ERRADO

// No React/Angular/frontend
const API_KEY = 'sk_live_abc123';
fetch(url, { headers: {
Authorization: `Bearer ${API_KEY}`
}});

CERTO

// No frontend
fetch('/api/calls', {
method: 'POST',
body: JSON.stringify({ name })
});
// API Key fica no servidor

Sempre use um backend proxy

Toda comunicação com a API da videochamada.com.br deve passar pelo seu backend. No Next.js, use API Routes. No React (Vite) e Angular, use um servidor Express, NestJS, Supabase Edge Functions, ou Cloudflare Workers. O frontend só precisa chamar o seu backend — a API Key nunca cruza a rede do lado do cliente.

Valide o acesso antes de criar chamadas

No seu backend proxy, verifique se o usuário autenticado tem permissão para criar uma chamada. Se você tem um sistema de agendamentos, valide que o agendamento existe, pertence ao usuário, e está no horário correto antes de gerar o link. Isso evita que qualquer pessoa autenticada crie chamadas ilimitadas.

Use URLs de curta duração

Os links de chamada gerados pela API expiram automaticamente após o uso. Não armazene links no frontend por longos períodos — gere um novo link no momento que o usuário clica em "Iniciar Chamada". Isso garante que links compartilhados acidentalmente não fiquem acessíveis para sempre.

Comparação dos Métodos

Cada método descrito neste guia tem vantagens diferentes. Use esta tabela para escolher o que melhor se encaixa no momento do seu projeto — você sempre pode evoluir depois sem reescrever o que já fez.

CritérioIframe SimplesAPI + IframeIntegração Completa
Tempo de implementação5 min15 min30 min
Segurança da API KeyExposta no frontendProtegida no servidorProtegida no servidor
ComplexidadeMínimaBaixaMédia
Personalização visualWhitelabelWhitelabelWhitelabel + wrapper UI
Gravação e transcriçãoVia painelVia API + webhooksVia API + webhooks
Controle do ciclo de vidaNenhumTotalTotal
Ideal paraMVPs, protótiposMaioria dos projetosApps de produção robustos

Recomendação:

Comece com o Método 2 (API + Iframe) — oferece o melhor equilíbrio entre segurança, velocidade de implementação e controle. Se estiver em modo protótipo urgente, use o Método 1 e migre depois. Adicione os recursos avançados (gravação, webhooks) conforme a demanda aparecer.

Perguntas Frequentes

Dúvidas comuns de desenvolvedores sobre a integração

Sobre Este Guia

Este guia foi escrito em Março de 2026 e testado com Next.js 14 (Pages e App Router), React 18 (Vite 5), e Angular 17. Os exemplos de código são completos e funcionais — você pode copiar e colar diretamente no seu projeto. A lógica de integração (backend proxy + iframe) é estável e não muda entre versões dos frameworks.

videochamada.com.br, Next.js, React, Vite e Angular são produtos independentes. Este guia é uma documentação técnica para ajudar desenvolvedores que usam esses frameworks a integrar videochamadas rapidamente.

Crie Sua Primeira Chamada Agora

Conta gratuita, 2.000 minutos/mês, sem cartão de crédito. Em 15 minutos, seu app Next.js, React ou Angular terá videochamadas profissionais.