VoxLegal — Deepgram Webhook Handler

Transcripción asíncrona de llamadas legales · Express.js + Supabase

Skill: deepgram-webhooks-transcripcion-asincrona
Llamadas transcritas hoy
47
↑ 12% vs ayer
Tasa de éxito webhook
99.1%
2 reintentos en 30 días
Latencia media callback
3.2s
Deepgram → endpoint
Flujo de transcripción asíncrona
📞
Llamada grabada
WebRTC · WAV/MP3
📤
POST Deepgram API
?callback=URL · nova-2-es
⚙️
Deepgram procesa
Retorna request_id
🔔
Webhook callback
POST /webhooks/deepgram
🗄️
Guarda en Supabase
tabla transcriptions
📧
Notifica abogado
SendGrid email
📄

webhook.handler.js — VoxLegal

src/routes/webhooks/deepgram.js JavaScript
import { createClient } from '@supabase/supabase-js';
import sgMail from '@sendgrid/mail';

const supabase = createClient(
  process.env.SUPABASE_URL,
  process.env.SUPABASE_SERVICE_KEY
);

// ── Middleware: verificar dg-token ──────────────────
const verifyDgToken = (req, res, next) => {
  const dgToken = req.headers['dg-token'];
  if (!dgToken) {
    return res.status(401).json({ error: 'Missing dg-token' });
  }
  if (dgToken !== process.env.DEEPGRAM_API_KEY_ID) {
    return res.status(403).json({ error: 'Invalid dg-token' });
  }
  next();
};

// ── Endpoint principal ──────────────────────────────
router.post(
  '/webhooks/deepgram',
  express.raw({ type: 'application/json' }),
  verifyDgToken,
  async (req, res) => {
    try {
      const result = JSON.parse(req.body.toString());
      const { request_id, results, duration } = result;
      const transcript =
        results.channels[0].alternatives[0].transcript;
      const confidence =
        results.channels[0].alternatives[0].confidence;

      // 1. Guardar en Supabase
      const { data, error } = await supabase
        .from('transcriptions')
        .update({
          status:     'completed',
          transcript,
          confidence,
          duration_s:  duration,
          completed_at: new Date().toISOString(),
        })
        .eq('deepgram_request_id', request_id)
        .select('lawyer_email, case_ref');

      if (error) throw error;

      // 2. Notificar al abogado (SendGrid)
      if (data?.[0]?.lawyer_email) {
        await sgMail.send({
          to:      data[0].lawyer_email,
          from:    'actas@voxlegal.io',
          subject: `Acta lista · Expediente ${data[0].case_ref}`,
          text:    `Transcripción completada. Confianza: ${confidence}`,
        });
      }

      // Deepgram NO reintentará si devolvemos 2xx
      return res.status(200).json({ ok: true });

    } catch (err) {
      console.error('[deepgram-webhook] Error:', err);
      // Retornar 5xx → Deepgram reintentará hasta 10x
      return res.status(500).json({ error: 'Processing failed' });
    }
  }
);
📦

Payload Deepgram (ejemplo real)

POST /webhooks/deepgram · dg-token: dg_ak_7x9Kp3mNq2 JSON
{
  "request_id": "a3e8f2c1-9b4d-4f2a-8e1c-7d3b2a9f0e55",
  "created":    "2026-06-16T11:23:44.182Z",
  "duration":   487.34,
  "channels":   2,
  "results": {
    "channels": [{
      "alternatives": [{
        "transcript": "Buenos días, señor Martínez.
Tal como acordamos, la cláusula de
no competencia quedará limitada a
un radio de cincuenta kilómetros
durante un período máximo de dos
años tras la extinción del contrato.",
        "confidence":  0.9873,
        "words":       "[... 894 words ...]"
      }]
    }]
  }
}
📋

Logs del servidor (Railway)

11:23:44.201 INFO POST /webhooks/deepgram recibido · IP: 52.41.42.151
11:23:44.203 AUTH dg-token válido · API Key ID: dg_ak_7x9Kp3mNq2
11:23:44.205 INFO request_id: a3e8f2c1 · duración: 487.34s · confianza: 98.73%
11:23:44.231 DB Supabase UPDATE OK · expediente EXP-2026-0847
11:23:44.419 EMAIL SendGrid → lcalderon@voxlegal.io · Acta lista · EXP-2026-0847
11:23:44.421 OK HTTP 200 · Deepgram NO reintentará · latencia: 220ms
🔐

Variables de entorno (.env)

Las variables marcadas con (*) son SECRETAS — nunca al repositorio.
DEEPGRAM_API_KEY= dg_prod_xxxxxxxxxxxxxxxxxxxx (*) Para solicitar transcripciones
DEEPGRAM_API_KEY_ID= dg_ak_7x9Kp3mNq2 Del panel Deepgram · valida dg-token
WEBHOOK_URL= https://voxlegal.railway.app/webhooks/deepgram Puerto 443 (HTTPS)
SUPABASE_URL= https://xyzcompany.supabase.co (*) Base de datos
SUPABASE_SERVICE_KEY= eyJhbGciOiJIUzI1NiIsInR5cCI... (*) Service role key
SENDGRID_API_KEY= SG.xxxxxxxxxxxxxxxxxxxxx (*) Notificaciones email
📊

Restricciones y comportamiento Deepgram

Deepgram solo permite callbacks en puertos 80, 443, 8080 y 8443.
Comportamiento Detalle VoxLegal
Puertos permitidos 80, 443, 8080, 8443 443 OK
Reintentos ante fallo Hasta 10 veces Manejado
Delay entre reintentos 30 segundos Idempotente
Autenticación dg-token header Verificado
HMAC signature No disponible Token-based
Respuesta esperada HTTP 200-299 200 JSON
Petición de transcripción con callback
💻

curl — Enviar audio a Deepgram con callback

Terminal · enviar llamada grabada a Deepgram bash
# VoxLegal: transcribir llamada EXP-2026-0847 con callback a Railway
curl \
  --request POST \
  --header 'Authorization: Token $DEEPGRAM_API_KEY' \
  --header 'Content-Type: audio/wav' \
  --data-binary @llamada-exp-2026-0847.wav \
  --url 'https://api.deepgram.com/v1/listen?
    model=nova-2&
    language=es&
    diarize=true&
    punctuate=true&
    callback=https://voxlegal.railway.app/webhooks/deepgram'

# Respuesta inmediata (Deepgram procesa en background):
# { "request_id": "a3e8f2c1-9b4d-4f2a-8e1c-7d3b2a9f0e55" }
# → Deepgram enviará el resultado al webhook cuando termine

# Desarrollo local con Hookdeck CLI:
npx hookdeck-cli listen 3000 deepgram --path /webhooks/deepgram
# Genera: https://events.hookdeck.com/e/src_XXXX → localhost:3000