Saltar al contenido principal

Integración con Llamalitica

Este documento describe el proceso de integración con Llamalitica utilizando nuestra API REST y el sistema de redirección a la plataforma. Este método ofrece una forma sencilla y eficiente de integrar las capacidades de Llamalitica en tu software.

La integración se realiza en tres pasos principales:

  1. Obtención del token de usuario mediante impersonación
  2. Creación de un caso con datos del paciente y documentos
  3. Redirección a la plataforma Llamalitica

Configuración Previa

1. Obtención de API Key

Para obtener tu API Key, accede al panel de desarrolladores en la sección de Developers -> API Keys. Esta API Key será necesaria para todas las interacciones con la API de Llamalitica.

Dominios de Llamalitica
  • api.llamalitica.com: Dominio para todas las llamadas a la API REST
  • llamalitica.com: Dominio para la interfaz web y el panel de control
API Keys Management

Almacenamiento Seguro de la API Key

Es fundamental almacenar la API Key de manera segura para evitar filtraciones y accesos no autorizados. Aquí hay algunas recomendaciones sobre cómo manejarla:

  1. Almacenamiento en el Servidor: La API Key nunca debe ser expuesta en el frontend. Debe almacenarse en el servidor, preferiblemente en variables de entorno o en un sistema de gestión de secretos.

  2. Rotación de Claves: Si sospechas que la API Key ha sido comprometida, es crucial borrar la clave actual y generar una nueva.

  3. Uso de HTTPS: Asegúrate de que todas las comunicaciones que involucren la API Key se realicen a través de HTTPS para proteger la información en tránsito.

  4. Principio de Mínimos Privilegios: Limita el acceso a la API Key solo a aquellos servicios y usuarios que realmente lo necesiten. Esto reduce el riesgo de exposición.

API Key

2. Configuración de Webhooks

Los webhooks te permiten recibir notificaciones en tiempo real sobre eventos en la plataforma.

Listado de webhooks

Configura tus webhooks en la sección de Developers -> Webhooks. Aquí podrás:

  • Especificar la URL de tu endpoint
  • Seleccionar los eventos que quieres recibir:
    • report.created: Se dispara cuando el agente IA genera un informe inicialmente, sin revisión ni modificación del profesional.
    • report.updated: Se dispara cada vez que el profesional guarda cambios en el informe.
    • report.deleted: Se dispara cuando se borra un informe del timeline.
  • Obtener el secreto para validar las firmas de los webhooks
Webhooks Management

Proceso de Integración Paso a Paso

Paso 1: Obtención del Token de Usuario

El primer paso es obtener un token de usuario mediante el endpoint de autenticación. Este token será necesario para todas las operaciones subsiguientes.

Se trata de hacer una impersonación de usuario para obtener su token de acceso, usando tu API Key de organización y el email del usuario a autenticar.

Endpoint de Autenticación

GET /api/users/me
X-API-KEY: {tu-api-key} # Requerido: Tu API Key de Llamalitica
X-USER-EMAIL: {email-del-usuario} # Requerido: Email del usuario a autenticar
X-ORGANIZATION: {id-suborganizacion} # Opcional: ID de la suborganización
Creación Automática

Si el usuario o la suborganización no existen, se crearán automáticamente. El usuario quedará vinculado a la organización especificada.

Ejemplos de Implementación

Ver ejemplo en TypeScript (Backend)
import axios from 'axios';

interface LlamaliticaUser {
accessToken: string;
id: string;
email: string;
name: string;
organization: {
id: string;
name: string;
settings: Record<string, any>;
};
specialities: string[];
agents: Array<{
id: string;
name: string;
description: string;
type: string;
}>;
}

async function getUserToken(
apiKey: string,
userEmail: string,
organizationId?: string
): Promise<LlamaliticaUser> {
const headers: Record<string, string> = {
'X-API-KEY': apiKey,
'X-USER-EMAIL': userEmail,
'Content-Type': 'application/json'
};

if (organizationId) {
headers['X-ORGANIZATION'] = organizationId;
}

try {
const response = await axios.get<{ user: LlamaliticaUser }>(
'https://api.llamalitica.com/api/users/me',
{ headers }
);

return response.data.user;
}
catch (error) {
if (axios.isAxiosError(error)) {
if (error.response?.status === 401) {
throw new Error('API Key o email inválidos');
}
throw new Error(`Error al obtener el token: ${error.response?.data?.message || error.message}`);
}
throw error;
}
}
Ver ejemplo en PHP
<?php

class LlamaliticaService {
private string $apiKey;
private \GuzzleHttp\Client $client;

public function __construct(string $apiKey) {
$this->apiKey = $apiKey;
$this->client = new \GuzzleHttp\Client([
'base_uri' => 'https://api.llamalitica.com'
]);
}

/**
* Obtener token de usuario
*
* @param string $userEmail Email del usuario
* @param string|null $organizationId ID de la organización (opcional)
* @return array Datos del usuario incluyendo el token
* @throws \Exception
*/
public function getUserToken(string $userEmail, ?string $organizationId = null): array {
try {
$headers = [
'X-API-KEY' => $this->apiKey,
'X-USER-EMAIL': $userEmail,
'Content-Type' => 'application/json'
];

if ($organizationId) {
$headers['X-ORGANIZATION'] = $organizationId;
}

$response = $this->client->get('/api/users/me', [
'headers' => $headers
]);

return json_decode($response->getBody()->getContents(), true)['user'];
} catch (\GuzzleHttp\Exception\ClientException $e) {
if ($e->getResponse()->getStatusCode() === 401) {
throw new \Exception('API Key o email inválidos');
}
throw new \Exception('Error al obtener el token: ' . $e->getMessage());
}
}
}
Ver ejemplo en Python
from dataclasses import dataclass
from typing import List, Dict, Optional
import httpx
from pathlib import Path

@dataclass
class CaseFile:
path: Path
name: str
type: str

class CaseService:
def __init__(self, api_key: str):
self.api_key = api_key
self.base_url = "https://api.llamalitica.com"

async def get_user_token(
self,
user_email: str,
organization_id: Optional[str] = None
) -> Dict:
"""
Obtener token de usuario de Llamalitica

Args:
user_email: Email del usuario
organization_id: ID de la organización (opcional)

Returns:
Dict: Datos del usuario incluyendo el token

Raises:
Exception: Si hay un error en la petición
"""
headers = {
"X-API-KEY": self.api_key,
"X-USER-EMAIL": user_email,
"Content-Type": "application/json"
}

if organization_id:
headers["X-ORGANIZATION"] = organization_id

async with httpx.AsyncClient() as client:
try:
response = await client.get(
f"{self.base_url}/api/users/me",
headers=headers
)
response.raise_for_status()
return response.json()["user"]
except httpx.HTTPStatusError as e:
if e.response.status_code == 401:
raise ValueError("API Key o email inválidos")
raise ValueError(f"Error al obtener el token: {e.response.text}")

Respuesta de Autenticación

{
"user": {
"accessToken": "eyJhbGciOiJIUzI1NiIs...", // Token JWT para usar en peticiones posteriores
"id": "user-uuid", // ID único del usuario
"email": "usuario@ejemplo.com", // Email del usuario
"name": "Nombre Usuario", // Nombre del usuario
"organization": {
"id": "org-uuid", // ID de la organización
"name": "Nombre Organización", // Nombre de la organización
"settings": { // Configuración de la organización
// ... detalles de configuración
}
},
"specialities": ["especialidad1", "especialidad2"], // Especialidades del usuario
"agents": [ // Agentes disponibles para el usuario
{
"id": "agent-uuid",
"name": "Nombre del Agente",
"description": "Descripción del agente",
"type": "report"
}
]
}
}

Paso 2: Creación del Caso

Una vez autenticado el usuario, procedemos a crear un nuevo caso clínico. Este paso permite enviar tanto los datos del paciente como los documentos asociados.

Endpoint de Creación de Caso

Documentación interactiva del endpoint de creación de caso

POST /api/cases
Content-Type: multipart/form-data

Parámetros del Formulario

  1. patientMetadata (requerido): Objeto JSON con los datos del paciente

    {
    "id": "id-interno-paciente", // Tu ID interno del paciente
    "name": "Nombre Paciente", // Nombre que aparecerá en el caso
    "otros-datos-clinicos": { // Opcional: Cualquier dato adicional
    "age": 45,
    "gender": "M",
    "allergies": ["penicilina"],
    "conditions": ["hipertensión"]
    }
    }
  2. files (opcional): Array de archivos a procesar

    • Cualquier formato soportado: PDF, DOCX, TXT, MP3, MP4, WAV, etc...
    • Tamaño máximo recomendado: 10MB por archivo
    • Sin límite en el número de archivos por caso

Ejemplos de Implementación

Ver ejemplo en TypeScript (Backend)
import axios from 'axios';
import FormData from 'form-data';
import { createReadStream } from 'node:fs';

interface PatientMetadata {
id: string;
name: string;
[key: string]: any;
}

interface CaseFile {
path: string;
name: string;
type: string;
}

async function createCase(
apiKey: string,
userEmail: string,
patientData: PatientMetadata,
files: CaseFile[]
): Promise<{
caseId: string;
patientId: string;
title: string;
createdAt: string;
files: Array<{
id: string;
name: string;
type: string;
size: number;
status: string;
addedAt: string;
}>;
}> {
const formData = new FormData();

// Añadir metadata del paciente
formData.append('patientMetadata', JSON.stringify(patientData));

// Añadir archivos
files.forEach((file) => {
formData.append('files', createReadStream(file.path), {
filename: file.name,
contentType: file.type
});
});

try {
const response = await axios.post(
'https://api.llamalitica.com/api/cases',
formData,
{
headers: {
'X-API-KEY': apiKey,
'X-USER-EMAIL': userEmail,
...formData.getHeaders()
}
}
);

return response.data;
}
catch (error) {
if (axios.isAxiosError(error)) {
throw new Error(`Error creating case: ${error.response?.data?.message || error.message}`);
}
throw error;
}
}
Ver ejemplo en PHP
<?php

class CaseService {
private string $apiKey;
private \GuzzleHttp\Client $client;

public function __construct(string $apiKey) {
$this->apiKey = $apiKey;
$this->client = new \GuzzleHttp\Client([
'base_uri' => 'https://api.llamalitica.com'
]);
}

/**
* Crear un nuevo caso
*
* @param string $userEmail Email del usuario
* @param array $patientData Datos del paciente
* @param array $files Array de archivos [['path' => string, 'name' => string]]
* @return array Datos del caso creado
* @throws \Exception
*/
public function createCase(
string $userEmail,
array $patientData,
array $files = []
): array {
try {
$multipart = [
[
'name' => 'patientMetadata',
'contents' => json_encode($patientData)
]
];

foreach ($files as $file) {
$multipart[] = [
'name' => 'files',
'contents' => fopen($file['path'], 'r'),
'filename' => $file['name']
];
}

$response = $this->client->post('/api/cases', [
'multipart' => $multipart,
'headers' => [
'X-API-KEY' => $this->apiKey,
'X-USER-EMAIL' => $userEmail
]
]);

return json_decode($response->getBody()->getContents(), true);
} catch (\Exception $e) {
throw new \Exception('Error creating case: ' . $e->getMessage());
}
}
}
Ver ejemplo en Python
from dataclasses import dataclass
from typing import List, Dict, Optional
import httpx
from pathlib import Path

@dataclass
class CaseFile:
path: Path
name: str
type: str

class CaseService:
def __init__(self, api_key: str):
self.api_key = api_key
self.base_url = "https://api.llamalitica.com"

async def create_case(
self,
user_email: str,
patient_data: Dict,
files: Optional[List[CaseFile]] = None
) -> Dict:
"""
Crear un nuevo caso

Args:
user_email: Email del usuario
patient_data: Datos del paciente
files: Lista opcional de archivos a procesar

Returns:
Dict: Datos del caso creado

Raises:
Exception: Si hay un error en la petición
"""
files = files or []

# Preparar formulario multipart
form = {
'patientMetadata': (
None,
json.dumps(patient_data),
'application/json'
)
}

# Añadir archivos
for i, file in enumerate(files):
form[f'files'] = (
file.name,
file.path.read_bytes(),
file.type
)

headers = {
'X-API-KEY': self.api_key,
'X-USER-EMAIL': user_email
}

async with httpx.AsyncClient() as client:
try:
response = await client.post(
f"{self.base_url}/api/cases",
files=form,
headers=headers
)
response.raise_for_status()
return response.json()
except httpx.HTTPError as e:
raise Exception(f"Error creating case: {str(e)}")

Paso 3: Redirección a Llamalitica

Una vez obtenido el ID del caso, puedes redirigir al usuario a la plataforma Llamalitica. La redirección se realiza al dominio de la interfaz web.

URL Base y Configuración

https://llamalitica.com/dashboard/cases/{caseId}?config={configBase64}

La configuración se envía como un objeto JSON codificado en base64 en el parámetro config:

{
"accessToken": "{accessToken}", // Token de acceso obtenido en el paso 1
"redirectUrl": "{urlDeRetorno}", // URL a la que redirigir tras guardar
"ui": {
"showSidebar": true, // Mostrar barra lateral
"showUploadButton": true, // Permitir subida de archivos
"showAudioRecorder": true, // Mostrar grabadora de audio
"showThemeToggle": true, // Permitir cambio de tema
"showChat": true, // Mostrar chat con la historia
"showEditorAIButton": true, // Mostrar botón de IA en editor
"showPdfDownloadButton": true, // Permitir descarga de PDF
"defaultAgent": { // Agente por defecto a ejecutar
"id": "agent-uuid",
"when": "afterLoad" // 'afterLoad' o 'afterStopRecording'
},
"colors": {
"primary": "#000000", // Color primario de la interfaz
"foreground": "#ffffff", // Color de primer plano
"background": "#f0f0f0" // Color de fondo
}
}
}

Consideraciones importantes sobre la configuración UI

Sobre los parámetros del objeto ui

Todos los parámetros dentro del objeto ui son completamente opcionales. Si no se proporciona alguno de ellos, la plataforma utilizará los valores por defecto, ofreciendo una experiencia estándar al usuario.

Personalización de colores
  • El objeto colors permite personalizar los colores de la interfaz
  • Importante: Al proporcionar una configuración personalizada de colores, se deshabilitará automáticamente la opción de alternar entre temas claro y oscuro en la plataforma
  • Si deseas mantener la funcionalidad de cambio de tema, omite el objeto colors en la configuración
Configuración de agente por defecto

El objeto defaultAgent permite especificar qué agente se ejecutará automáticamente:

  • id: Debe ser un identificador válido de un agente que esté disponible para el usuario
  • when: Define cuándo se ejecutará automáticamente el agente:
    • afterLoad: Inmediatamente después de cargar el caso
    • afterStopRecording: Después de detener una grabación de audio

Recomendación: Se aconseja no especificar este parámetro para permitir que sea el usuario quien decida qué agente utilizar y cuándo ejecutarlo, mejorando así la experiencia de usuario y evitando comportamientos inesperados.

Ejemplos de Codificación Base64

La codificación base64 es necesaria para enviar la configuración de manera segura en la URL. Aquí tienes ejemplos de cómo realizarla en diferentes lenguajes:

Ver ejemplo de codificación base64 en JavaScript/TypeScript
import { encode as base64Encode } from 'base64url';

function generateRedirectUrl(
caseId: string,
accessToken: string,
redirectUrl: string
): string {
const config = {
accessToken,
redirectUrl,
ui: {
showSidebar: true,
showUploadButton: true,
showEditorAIButton: true,
defaultAgent: {
id: 'agent-uuid',
when: 'afterLoad'
},
colors: {
primary: '#4a90e2',
foreground: '#333333',
background: '#f5f5f5'
}
}
};

const configBase64 = base64Encode(JSON.stringify(config));
return `https://llamalitica.com/dashboard/cases/${caseId}?config=${configBase64}`;
}
Ver ejemplo de codificación base64 en PHP
function generateRedirectUrl(
string $caseId,
string $accessToken,
string $redirectUrl
): string {
$config = [
'accessToken' => $accessToken,
'redirectUrl' => $redirectUrl,
'ui' => [
'showSidebar' => true,
'showUploadButton' => true,
'showEditorAIButton' => true,
'defaultAgent' => [
'id' => 'agent-uuid',
'when' => 'afterLoad'
],
'colors' => [
'primary' => '#4a90e2',
'foreground' => '#333333',
'background' => '#f5f5f5'
]
]
];

$configBase64 = base64_encode(json_encode($config));
return sprintf(
'https://llamalitica.com/dashboard/cases/%s?config=%s',
$caseId,
urlencode($configBase64)
);
}
Ver ejemplo de codificación base64 en Python
import base64
import json
from urllib.parse import quote

def generate_redirect_url(case_id: str, access_token: str, redirect_url: str) -> str:
"""Genera la URL de redirección a Llamalitica"""
config = {
'accessToken': access_token,
'redirectUrl': redirect_url,
'ui': {
'showSidebar': True,
'showUploadButton': True,
'showEditorAIButton': True,
'defaultAgent': {
'id': 'agent-uuid',
'when': 'afterLoad'
},
'colors': {
'primary': '#4a90e2',
'foreground': '#333333',
'background': '#f5f5f5'
}
}
}

config_json = json.dumps(config)
config_base64 = base64.b64encode(config_json.encode()).decode()
return f"https://llamalitica.com/dashboard/cases/{case_id}?config={quote(config_base64)}"
Ver ejemplo de codificación base64 en Go
package base64helper

import (
"encoding/base64"
"encoding/json"
"net/url"
"fmt"
)

// Ejemplo de uso con la configuración actualizada
func buildRedirectUrl(caseId string, accessToken string, redirectUrl string) (string, error) {
config := map[string]interface{}{
"accessToken": accessToken,
"redirectUrl": redirectUrl,
"ui": map[string]interface{}{
"showSidebar": true,
"showUploadButton": true,
"showEditorAIButton": true,
"defaultAgent": map[string]interface{}{
"id": "agent-uuid",
"when": "afterLoad",
},
"colors": map[string]interface{}{
"primary": "#4a90e2",
"foreground": "#333333",
"background": "#f5f5f5",
},
},
}

encodedConfig, err := encodeConfigForUrl(config)
if err != nil {
return "", err
}

return fmt.Sprintf(
"https://llamalitica.com/dashboard/cases/%s?config=%s",
caseId,
encodedConfig,
), nil
}

func encodeConfigForUrl(config interface{}) (string, error) {
jsonBytes, err := json.Marshal(config)
if err != nil {
return "", err
}

base64Str := base64.StdEncoding.EncodeToString(jsonBytes)
return url.QueryEscape(base64Str), nil
}
Ver ejemplo de codificación base64 en Java
import java.util.Base64;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.HashMap;
import java.util.Map;

public class ConfigEncoder {
private final ObjectMapper objectMapper = new ObjectMapper();

public String encodeConfigForUrl(Object config) throws Exception {
// Convertir a JSON, luego a base64 y finalmente codificar para URL
byte[] jsonBytes = objectMapper.writeValueAsBytes(config);
String base64 = Base64.getEncoder().encodeToString(jsonBytes);
return URLEncoder.encode(base64, StandardCharsets.UTF_8.toString());
}

// Ejemplo de uso con la configuración actualizada
public String buildRedirectUrl(String caseId, String accessToken, String redirectUrl)
throws Exception {

Map<String, Object> colors = new HashMap<>();
colors.put("primary", "#4a90e2");
colors.put("foreground", "#333333");
colors.put("background", "#f5f5f5");

Map<String, Object> defaultAgent = new HashMap<>();
defaultAgent.put("id", "agent-uuid");
defaultAgent.put("when", "afterLoad");

Map<String, Object> ui = new HashMap<>();
ui.put("showSidebar", true);
ui.put("showUploadButton", true);
ui.put("showEditorAIButton", true);
ui.put("defaultAgent", defaultAgent);
ui.put("colors", colors);

Map<String, Object> config = new HashMap<>();
config.put("accessToken", accessToken);
config.put("redirectUrl", redirectUrl);
config.put("ui", ui);

String encodedConfig = encodeConfigForUrl(config);
return String.format(
"https://llamalitica.com/dashboard/cases/%s?config=%s",
caseId,
encodedConfig
);
}
}
Ver ejemplo de codificación base64 en C#
using System.Text;
using System.Text.Json;
using System.Web;
using System.Collections.Generic;

public class ConfigEncoder
{
public string EncodeConfigForUrl<T>(T config)
{
// Convertir a JSON, luego a base64 y finalmente codificar para URL
var json = JsonSerializer.Serialize(config);
var base64 = Convert.ToBase64String(
Encoding.UTF8.GetBytes(json)
);
return HttpUtility.UrlEncode(base64);
}

// Ejemplo de uso con la configuración actualizada
public string BuildRedirectUrl(string caseId, string accessToken, string redirectUrl)
{
var config = new Dictionary<string, object>
{
["accessToken"] = accessToken,
["redirectUrl"] = redirectUrl,
["ui"] = new Dictionary<string, object>
{
["showSidebar"] = true,
["showUploadButton"] = true,
["showEditorAIButton"] = true,
["defaultAgent"] = new Dictionary<string, object>
{
["id"] = "agent-uuid",
["when"] = "afterLoad"
},
["colors"] = new Dictionary<string, object>
{
["primary"] = "#4a90e2",
["foreground"] = "#333333",
["background"] = "#f5f5f5"
}
}
};

var encodedConfig = EncodeConfigForUrl(config);
return $"https://llamalitica.com/dashboard/cases/{caseId}?config={encodedConfig}";
}
}

Gestión de Eventos

Una vez que el usuario está en la plataforma Llamalitica, puede realizar diversas acciones que generarán eventos. Estos eventos se enviarán a tu webhook configurado.

Formato de los Eventos

Cada evento enviado a tu webhook tendrá la siguiente estructura:

{
"event": "report.created", // Tipo de evento
"organizationId": "org-uuid", // ID de la organización
"timestamp": "2024-03-21T10:00:00Z", // Fecha y hora del evento
"data": {
"id": "report-uuid", // ID del informe
"caseId": "case-uuid", // ID del caso
"agent": {
"id": "agent-uuid", // ID del agente usado
"name": "Nombre del Agente" // Nombre del agente
},
"content": "Contenido del informe", // Contenido generado
"structuredData": { // Información estructurada del informe
// ... estructura según el agente seleccionado
}
}
}

Validación de Eventos

Para garantizar la seguridad de tu aplicación, es fundamental verificar que los eventos que recibes realmente provienen de Llamalitica. Esto es especialmente importante porque los webhooks son endpoints públicos que podrían ser objetivo de ataques.

¿Cómo funciona la validación?

  1. Cuando configuras un webhook, Llamalitica te proporciona un secreto único
  2. Cada vez que enviamos un evento a tu webhook, incluimos una firma en el header X-Webhook-Signature
  3. Esta firma se genera usando tu secreto y el contenido del evento
  4. Tu aplicación debe validar esta firma para asegurarse de que el evento es auténtico

Ejemplo paso a paso

  1. Obtén la firma del evento:

    • La firma viene en el header X-Webhook-Signature
    • Es una cadena en formato hexadecimal
  2. Genera la firma esperada:

    • Toma el cuerpo del evento (el JSON completo)
    • Usa el secreto de tu webhook para crear un hash HMAC SHA-256
    • Convierte el resultado a formato hexadecimal
  3. Compara las firmas:

    • Si las firmas coinciden, el evento es auténtico
    • Si no coinciden, debes rechazar el evento

Ejemplos de implementación

Ver ejemplo en TypeScript
import crypto from 'node:crypto';

function validateWebhookSignature(
webhookSecret: string,
payload: unknown,
receivedSignature: string
): boolean {
// Crear un hash HMAC usando el secreto
const hmac = crypto.createHmac('sha256', webhookSecret);

// Añadir el payload al hash
hmac.update(JSON.stringify(payload));

// Generar la firma esperada
const expectedSignature = hmac.digest('hex');

// Comparar con la firma recibida
return crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(receivedSignature)
);
}

// Ejemplo de uso en Express
app.post('/webhook', (req, res) => {
const signature = req.headers['x-webhook-signature'];

if (!signature || typeof signature !== 'string') {
return res.status(401).json({ error: 'Firma no proporcionada' });
}

const isValid = validateWebhookSignature(
process.env.WEBHOOK_SECRET!,
req.body,
signature
);

if (!isValid) {
return res.status(401).json({ error: 'Firma inválida' });
}

// El evento es válido, procesar según su tipo
const { event, data } = req.body;

switch (event) {
case 'report.created':
// Manejar creación de informe (generado inicialmente por la IA)
break;
case 'report.updated':
// Manejar actualización de informe (guardado por el profesional)
break;
case 'report.deleted':
// Manejar eliminación de informe (borrado del timeline)
break;
}

res.json({ received: true });
});
Ver ejemplo en PHP
function validateWebhookSignature(
string $webhookSecret,
string $payload,
string $receivedSignature
): bool {
// Generar la firma esperada
$expectedSignature = hash_hmac(
'sha256',
$payload,
$webhookSecret
);

// Comparar con la firma recibida
return hash_equals($expectedSignature, $receivedSignature);
}

// Ejemplo de uso
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';

if (!validateWebhookSignature(
$_ENV['WEBHOOK_SECRET'],
$payload,
$signature
)) {
http_response_code(401);
echo json_encode(['error' => 'Firma inválida']);
exit;
}

// El evento es válido, procesar
$event = json_decode($payload, true);
// ... procesar el evento
Ver ejemplo en Python
import hmac
import hashlib
from fastapi import FastAPI, Request, HTTPException
from typing import Dict

app = FastAPI()

def validate_webhook_signature(
webhook_secret: str,
payload: bytes,
received_signature: str
) -> bool:
"""Valida la firma de un evento webhook"""
# Generar la firma esperada
expected_signature = hmac.new(
webhook_secret.encode(),
payload,
hashlib.sha256
).hexdigest()

# Comparar con la firma recibida
return hmac.compare_digest(
expected_signature,
received_signature
)

@app.post("/webhook")
async def webhook(request: Request):
# Obtener la firma del header
signature = request.headers.get("x-webhook-signature")
if not signature:
raise HTTPException(401, "Firma no proporcionada")

# Leer el payload como bytes
payload = await request.body()

# Validar la firma
if not validate_webhook_signature(
webhook_secret="tu-secreto-aqui",
payload=payload,
received_signature=signature
):
raise HTTPException(401, "Firma inválida")

# El evento es válido, procesar
event = await request.json()
# ... procesar el evento

return {"received": True}
Buenas prácticas
  • Usa comparación segura contra ataques de timing (como crypto.timingSafeEqual o hash_equals)
  • Almacena el secreto del webhook de forma segura (variables de entorno, gestor de secretos, etc.)
  • Rechaza eventos sin firma o con firma inválida

Consideraciones de Seguridad

  • API Key:

    • Almacena la API Key de forma segura en variables de entorno
    • Nunca la expongas en el frontend o en código público
    • Rota la clave periódicamente o si sospechas que ha sido comprometida
  • Webhooks:

    • Valida siempre la firma de los eventos recibidos
    • Usa HTTPS para tu endpoint de webhook
  • Tokens de Usuario:

    • Los tokens son temporales y específicos para cada sesión

Preguntas Frecuentes

¿Cómo obtengo las credenciales necesarias?

  1. Solicita acceso como desarrollador desde nuestra web
  2. Una vez aprobada tu solicitud, tendrás acceso a la sección de desarrolladores
  3. En el panel de desarrolladores podrás:
    • Generar y gestionar tus API Keys
    • Configurar webhooks
    • Acceder a la documentación completa
  4. Guarda de forma segura las credenciales

¿Puedo personalizar la interfaz de Llamalitica?

Sí, mediante el objeto de configuración UI puedes personalizar varios aspectos:

  • Visibilidad de componentes
  • Colores del tema
  • Plantilla por defecto
  • Funcionalidades disponibles

¿Qué sucede si el token expira?

El token generado tiene un tiempo de caducidad suficiente para que el usuario llegue a la página de Llamalitica. Una vez en la plataforma, no necesitas preocuparte por la renovación del token, ya que la propia aplicación web se encarga de gestionar y renovar los tokens automáticamente para mantener la sesión activa.

¿Cómo gestiono los errores?

Implementa manejo de errores para las llamadas a la API y verifica los códigos de estado HTTP:

  • 401: Token inválido o expirado
  • 402: Créditos insuficientes
  • 404: Recurso no encontrado
  • 500: Error interno del servidor

Soporte

Para cualquier duda o problema durante la integración, puedes: