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:
- Lea el header
X-Diazluna-Signature. - Extraiga
tyv1separando por,. -
Verifique que
testé dentro de los últimos 5 minutos (protección contra replay). -
Calcule
HMAC_SHA256(secreto, t + "." + cuerpo_raw_string)en hex. -
Compare con
v1usando comparación de tiempo constante (evita timing attacks). Su lenguaje suele tener una función dedicada:crypto.timingSafeEqualen Node,hmac.compare_digesten Python,hash_equalsen 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_type | Cuándo se dispara |
|---|---|
call.captured | María registró una llamada (con o sin transferencia) |
call.transferred | María transfirió la llamada al equipo de la práctica |
call.transfer_failed | María intentó transferir y nadie contestó |
call.appointment_requested | El motivo categorizado fue scheduling |
conversation.message_received | WhatsApp entrante de un paciente/cliente |
conversation.escalation_created | El bot escaló a humano (frustración, cancelación, etc.) |
content.published | El admin publicó un cambio del sitio (chat o WhatsApp) |
cap.threshold_reached | Su uso mensual cruzó 80% o 100% del cap del plan |
subscription.changed | Su suscripción Stripe cambió (próximamente) |
customer.created | Conversió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.