Saltar al contenido

Integraciones

Diazluna se conecta con sus sistemas de dos formas: webhooks salientes (le enviamos eventos cuando algo sucede) y herramientas para María (la IA consulta sus datos en vivo durante una llamada). Esta página cubre el contrato técnico de ambos.

Webhooks salientes

Cuando algo sucede en Diazluna (una llamada se captura, su contenido se publica, su tier alcanza un umbral), enviamos un POST firmado con HMAC-SHA256 a la URL que usted registre en el panel admin bajo Integraciones → Webhooks.

Forma del envío

POST https://su-dominio.com/diazluna-webhook
Content-Type: application/json
X-Diazluna-Signature: t=1778302100,v1=a1b2c3d4...
User-Agent: Diazluna-Webhook/1

{
  "event_id": "9f8e2dc8-0cc0-9bf6-...",
  "event_type": "call.captured",
  "occurred_at": "2026-05-09T13:45:00Z",
  "customer_id": "11111111-2222-3333-4444-555555555555",
  "data": { ... }
}

Reintentos

Si su endpoint responde con un código no-2xx (o no responde dentro de 30 segundos), reintentamos con backoff exponencial: 1s → 5s → 30s → 5min → 30min. Después de 6 fallos consecutivos marcamos la entrega como dead y dejamos de reintentar. Puede reactivarla manualmente desde el panel admin.

Idempotencia

Cada evento tiene un event_id único que se mantiene a través de los reintentos. Si su sistema ya procesó un event_id, descártelo y responda 200 (no se preocupe por re-procesar).

Esquema de firma HMAC-SHA256

El header X-Diazluna-Signature sigue el mismo formato que Stripe:

X-Diazluna-Signature: t=<unix_seconds>,v1=<hmac_hex>

donde:
  t   = unix timestamp del momento en que firmamos
  v1  = HMAC_SHA256(su_secreto, "<t>.<cuerpo_raw>")

Para validar:

  1. Lea el header X-Diazluna-Signature.
  2. Extraiga t y v1 separando por ,.
  3. Verifique que t esté dentro de los últimos 5 minutos (protección contra replay).
  4. Calcule HMAC_SHA256(secreto, t + "." + cuerpo_raw_string) en hex.
  5. Compare con v1 usando comparación de tiempo constante (evita timing attacks). Su lenguaje suele tener una función dedicada: crypto.timingSafeEqual en Node, hmac.compare_digest en Python, hash_equals en PHP.

El secreto se le muestra una sola vez al crear la subscripción (o al rotar). Guárdelo en una variable de entorno como cualquier otra credencial. Si lo pierde, rote desde el panel admin.

Verificar la firma

Snippets listos para copiar:

Node.js (sin dependencias)

import crypto from "node:crypto";

export function verifyDiazlunaSignature(opts) {
  const { rawBody, headerValue, secret } = opts;
  const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(headerValue || "");
  if (!m) return false;
  const ts = Number(m[1]);
  const sig = m[2];

  // 5-minute replay window
  if (Math.abs(Math.floor(Date.now() / 1000) - ts) > 300) return false;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(\`\${ts}.\${rawBody}\`)
    .digest("hex");

  // Constant-time compare
  try {
    return crypto.timingSafeEqual(
      Buffer.from(expected, "hex"),
      Buffer.from(sig, "hex"),
    );
  } catch {
    return false;
  }
}

// In your route handler:
//   const rawBody = req.rawBody.toString("utf8"); // capture before JSON.parse
//   const sigHeader = req.headers["x-diazluna-signature"];
//   if (!verifyDiazlunaSignature({ rawBody, headerValue: sigHeader, secret: process.env.DIAZLUNA_WEBHOOK_SECRET })) {
//     return res.status(401).end();
//   }

Python 3 (sin dependencias)

import hmac
import hashlib
import re
import time

SIG_RE = re.compile(r"^t=(\d+),v1=([0-9a-f]{64})$")

def verify_diazluna_signature(raw_body: bytes, header_value: str, secret: str) -> bool:
    m = SIG_RE.match(header_value or "")
    if not m:
        return False
    ts = int(m.group(1))
    sig_hex = m.group(2)

    if abs(int(time.time()) - ts) > 300:
        return False

    expected = hmac.new(
        secret.encode("utf-8"),
        f"{ts}.{raw_body.decode('utf-8')}".encode("utf-8"),
        hashlib.sha256,
    ).hexdigest()

    return hmac.compare_digest(expected, sig_hex)

# In Flask, for example:
#   raw_body = request.get_data()  # bytes — DO NOT use request.json
#   sig_header = request.headers.get("X-Diazluna-Signature", "")
#   if not verify_diazluna_signature(raw_body, sig_header, os.environ["DIAZLUNA_WEBHOOK_SECRET"]):
#       abort(401)

PHP 8 (sin dependencias)

<?php
function verifyDiazlunaSignature(string $rawBody, string $headerValue, string $secret): bool {
    if (!preg_match('/^t=(\d+),v1=([0-9a-f]{64})$/', $headerValue, $m)) {
        return false;
    }
    $ts = (int) $m[1];
    $sigHex = $m[2];

    if (abs(time() - $ts) > 300) {
        return false;
    }

    $expected = hash_hmac('sha256', $ts . '.' . $rawBody, $secret);
    return hash_equals($expected, $sigHex);
}

// In your handler:
//   $rawBody = file_get_contents('php://input');
//   $sigHeader = $_SERVER['HTTP_X_DIAZLUNA_SIGNATURE'] ?? '';
//   if (!verifyDiazlunaSignature($rawBody, $sigHeader, getenv('DIAZLUNA_WEBHOOK_SECRET'))) {
//       http_response_code(401);
//       exit;
//   }

Importante: firme y verifique sobre el cuerpo raw (la string exacta que llegó), no sobre un JSON re-serializado. Re-serializar reordena claves y rompe la firma.

Catálogo de eventos v1

Estos son los event_type disponibles hoy:

event_typeCuándo se dispara
call.capturedMaría registró una llamada (con o sin transferencia)
call.transferredMaría transfirió la llamada al equipo de la práctica
call.transfer_failedMaría intentó transferir y nadie contestó
call.appointment_requestedEl motivo categorizado fue scheduling
conversation.message_receivedWhatsApp entrante de un paciente/cliente
conversation.escalation_createdEl bot escaló a humano (frustración, cancelación, etc.)
content.publishedEl admin publicó un cambio del sitio (chat o WhatsApp)
cap.threshold_reachedSu uso mensual cruzó 80% o 100% del cap del plan
subscription.changedSu suscripción Stripe cambió (próximamente)
customer.createdConversión completada (admin: useful para CRM sync)

Herramientas para María (entrante)

Mientras María está en una llamada, puede invocar funciones que usted defina — por ejemplo: "¿Hay disponibilidad el martes a las 2pm?". Diazluna firma y reenvía la llamada a la URL que usted configure, con un tiempo límite duro de 3 segundos. Si su endpoint excede el límite o falla, María dice una frase de respaldo configurable ("Permítame verificar eso con el equipo") y continúa la llamada sin error.

Registro

Configure herramientas en el panel admin bajo Integraciones → Herramientas. Cada herramienta requiere:

  • Nombre (lo que el LLM usa para invocar): check_appointment_availability
  • Descripción (lo que el LLM lee al decidir si usar la herramienta)
  • Esquema de parámetros (JSON Schema que describe los argumentos)
  • URL de su endpoint
  • Tiempo límite (1-5s, default 3s)
  • Frases de respaldo en español e inglés

Contrato del endpoint de herramienta

Lo que recibirá

POST https://su-dominio.com/diazluna-tool
Content-Type: application/json
X-Diazluna-Signature: t=1778302100,v1=a1b2c3d4...
User-Agent: Diazluna-Tool-Proxy/1

{
  "args": { "date": "2026-05-15", "time": "14:00" },
  "name": "check_appointment_availability",
  "call": {
    "call_id": "..."
  }
}

Verifique la firma con el mismo esquema de webhooks (los snippets arriba aplican tal cual).

Lo que debe responder

Responda con un JSON-objeto en menos de 3 segundos. María ve el cuerpo de su respuesta como el resultado de la herramienta y lo incorpora a la siguiente frase.

HTTP 200
Content-Type: application/json

{
  "available_slots": ["10:00", "14:00", "15:30"],
  "next_available_day": "2026-05-15"
}

Si necesita devolver texto plano, también funciona — lo envolvemos como { "result": "<su texto>" }.

Cuando algo falla

Si su endpoint tarda más de su timeout_ms, devuelve 4xx/5xx, o no responde, María dice la frase de respaldo y la llamada continúa. No reintentamos (las llamadas son en vivo). Cada invocación queda registrada en Integraciones → Herramientas → Ver invocaciones recientes para que pueda diagnosticar.

Soporte

Preguntas técnicas: [email protected]. Incluya su event_id o retell_call_id cuando reporte un problema — nos permite ubicarlo en los logs en segundos.