Skip to main content

Integration with Llamalitica

This document describes the integration process with Llamalitica using our REST API and the platform redirect system. This method provides a simple and efficient way to integrate Llamalitica's capabilities into your software.

The integration is performed in three main steps:

  1. Obtaining the user token through impersonation
  2. Creating a case with patient data and documents
  3. Redirecting to the Llamalitica platform
Prerequisites

Before starting the technical integration, we recommend reading the Hierarchy, Organizations, and Agents Guide to understand Llamalitica's organizational model.

Initial Setup

1. Obtaining an API Key

To get your API Key, access the developer panel under Developers -> API Keys. This API Key is required for all interactions with the Llamalitica API.

Llamalitica Domains
  • api.llamalitica.com: Domain for all REST API calls
  • llamalitica.com: Domain for the web interface and control panel
Point-to-point VPN connection

If your infrastructure requires data traffic to flow exclusively through a point-to-point VPN instead of the public internet, we can configure a dedicated tunnel that routes communication directly to Llamalitica.

This option is ideal for organizations with strict security policies or regulatory compliance requirements that mandate private connections.

Interested? Contact our team to assess your case and configure the VPN connection.

Once the VPN is established, you will need to update the domains in your integration:

Public connectionPrivate VPN connection
llamalitica.comprivate.llamalitica.com
api.llamalitica.comapi.private.llamalitica.com

Simply replace all references to public domains with their private equivalents in your integration code.

API Keys Management

Secure Storage of the API Key

It is essential to store the API Key securely to prevent leaks and unauthorized access. Here are some recommendations for handling it:

  1. Server-side storage: The API Key must never be exposed in the frontend. It must be stored on the server, preferably in environment variables or a secrets management system.

  2. Key rotation: If you suspect the API Key has been compromised, it is crucial to delete the current key and generate a new one.

  3. Use HTTPS: Ensure that all communications involving the API Key are performed over HTTPS to protect information in transit.

  4. Principle of Least Privilege: Limit access to the API Key only to those services and users who truly need it. This reduces the risk of exposure.

API Key

2. Configuring Webhooks

Webhooks allow you to receive real-time notifications about events on the platform.

Webhook list

Configure your webhooks under Developers -> Webhooks. Here you can:

  • Specify your endpoint URL
  • Select the events you want to receive:
    • report.created: Fired when the AI agent initially generates a report, without any review or modification by the professional.
    • report.updated: Fired each time the professional saves changes to the report.
    • report.deleted: Fired when a report is deleted from the timeline.
  • Obtain the secret for validating webhook signatures
Webhooks Management

Step-by-Step Integration Process

Step 1: Obtaining the User Token

The first step is to obtain a user token through the authentication endpoint. This token will be required for all subsequent operations.

This involves user impersonation to obtain their access token, using your organization API Key and the email of the user to authenticate.

Authentication Endpoint

GET /api/users/me
X-API-KEY: {your-api-key} # Required: Your Llamalitica API Key
X-USER-EMAIL: {user-email} # Required: Email of the user to authenticate
X-ORGANIZATION: {sub-organization-id} # Optional: Sub-organization ID
Automatic Creation

If the user or sub-organization does not exist, it will be created automatically. The user will be linked to the specified organization.

Implementation Examples

View example in TypeScript (Backend)
import axios from 'axios';

type 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('Invalid API Key or email');
}
throw new Error(
`Error obtaining token: ${error.response?.data?.message || error.message}`,
);
}
throw error;
}
}
View example in 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'
]);
}

/**
* Get user token
*
* @param string $userEmail User email
* @param string|null $organizationId Organization ID (optional)
* @return array User data including the 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('Invalid API Key or email');
}
throw new \Exception('Error obtaining token: ' . $e->getMessage());
}
}
}
View example in 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:
"""
Get Llamalitica user token

Args:
user_email: User email
organization_id: Organization ID (optional)

Returns:
Dict: User data including the token

Raises:
Exception: If there is an error in the request
"""
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("Invalid API Key or email")
raise ValueError(f"Error obtaining token: {e.response.text}")

Authentication Response

{
"user": {
"accessToken": "eyJhbGciOiJIUzI1NiIs...", // JWT token for use in subsequent requests
"id": "user-uuid", // Unique user ID
"email": "user@example.com", // User email
"name": "User Name", // User name
"organization": {
"id": "org-uuid", // Organization ID
"name": "Organization Name", // Organization name
"settings": {
// Organization settings
// ... configuration details
}
},
"specialities": ["specialty1", "specialty2"], // User specialties
"agents": [
// Agents available to the user
{
"id": "agent-uuid",
"name": "Agent Name",
"description": "Agent description",
"type": "report"
}
]
}
}

Step 2: Creating the Case

Once the user is authenticated, we proceed to create a new clinical case. This step allows you to send both patient data and associated documents.

Case Creation Endpoint

Interactive documentation for the case creation endpoint

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

Form Parameters

  1. patientMetadata (required): JSON object with patient data

    {
    "id": "internal-patient-id", // Your internal patient ID
    "name": "Patient Name", // Name that will appear on the case
    "other-clinical-data": {
    // Optional: Any additional data
    "age": 45,
    "gender": "M",
    "allergies": ["penicillin"],
    "conditions": ["hypertension"]
    }
    }
  2. files (optional): Array of files to process

    • Any supported format: PDF, DOCX, TXT, MP3, MP4, WAV, etc.
    • Recommended maximum size: 10MB per file
    • No limit on the number of files per case

Implementation Examples

View example in TypeScript (Backend)
import axios from 'axios';
import FormData from 'form-data';
import { createReadStream } from 'node:fs';

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

type 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();

// Add patient metadata
formData.append('patientMetadata', JSON.stringify(patientData));

// Add files
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;
}
}
View example in 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'
]);
}

/**
* Create a new case
*
* @param string $userEmail User email
* @param array $patientData Patient data
* @param array $files Array of files [['path' => string, 'name' => string]]
* @return array Created case data
* @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());
}
}
}
View example in 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:
"""
Create a new case

Args:
user_email: User email
patient_data: Patient data
files: Optional list of files to process

Returns:
Dict: Created case data

Raises:
Exception: If there is an error in the request
"""
files = files or []

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

# Add files
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)}")

Step 3: Redirecting to Llamalitica

Once you have the case ID, you can redirect the user to the Llamalitica platform. The redirect goes to the web interface domain. A minimalist widget is also available that only shows the audio recording option.

A visual interface is available to test different configurations from our developer tools -> Integrations.

Base URL and Configuration

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

The configuration is sent as a base64-encoded JSON object in the config parameter:

{
"accessToken": "{accessToken}", // Access token obtained in step 1
"redirectUrl": "{returnUrl}", // URL to redirect to after saving
"ui": {
"showSidebar": true, // Show sidebar
"showUploadButton": true, // Allow file uploads
"showAudioRecorder": true, // Show audio recorder
"showThemeToggle": true, // Allow theme switching
"showChat": true, // Show chat with history
"showEditorAIButton": true, // Show AI button in editor
"showPdfDownloadButton": true, // Allow PDF download
"saveAndFinishText": "Save and finish", // Text for the "Save and finish" button
"defaultAgent": {
// Default agent to run
"id": "{agent-uuid}",
"when": "afterLoad" // 'afterLoad' or 'afterStopRecording'
},
"availableAgents": ["agent-uuid-1", "agent-uuid-2"], // Optional array of available agent UUIDs
"telemedicine": {
// Telemedicine recorder mode configuration (optional)
"captureSurface": "fullPicker", // 'currentTab' or 'fullPicker'
"enabledByDefault": false // Start already in telemedicine mode
},
"colors": {
"primary": "#000000", // Primary interface color
"foreground": "#ffffff", // Foreground color
"background": "#f0f0f0" // Background color
}
}
}
Mini Audio Widget

In this widget, a report will be generated automatically when the audio recording ends.

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

The configuration is sent as a base64-encoded JSON object in the config parameter:

{
"accessToken": "{accessToken}", // Access token obtained in step 1
"redirectUrl": "{returnUrl}", // URL to redirect to after saving
"ui": {
"defaultAgent": {
// Default agent to run
"id": "{agent-uuid}"
},
"telemedicine": {
// Optional: also captures audio from the video call
"enabledByDefault": true, // Starts recording tab + microphone
"captureSurface": "currentTab" // Direct capture of the current tab (1 click)
}
}
}
Telemedicine in the mini-widget

If the video call (TokBox, Twilio, etc.) lives in the same tab that embeds the widget, use "captureSurface": "currentTab" so the recorder captures the current tab without showing the full browser picker. More details in Telemedicine Mode.

Embedded Chat Widget (iframe)

Integrate the chat of a case directly into your application via an <iframe>. In this mode only the chat is shown, without a sidebar or additional controls.

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

The minimum required configuration is:

{
"accessToken": "{accessToken}",
"ui": {
"chatOnly": true
}
}

With chatOnly: true no other ui parameters need to be specified.

Example usage with iframe:

<iframe
src="https://llamalitica.com/dashboard/cases/{caseId}?config={configBase64}"
width="400"
height="600"
style="border: none; border-radius: 8px"
allow="microphone" />
Visual generator

You can generate the URL and preview the widget from Developers → Integrations → Embedded Chat.

Widget configuration scope

The widget configuration is stored in the browser's sessionStorage, so it only affects the tab where the embed was opened. Other tabs of the application are not modified, and when the tab is closed the configuration is automatically removed.

Important considerations about the UI configuration

About ui object parameters

All parameters within the ui object are completely optional. If any of them is not provided, the platform will use default values, offering a standard user experience.

Color customization
  • The colors object allows you to customize the interface colors
  • Important: When providing a custom color configuration, the option to toggle between light and dark themes on the platform will be automatically disabled
  • If you want to keep the theme-switching functionality, omit the colors object from the configuration
"Save and finish" button text

The saveAndFinishText parameter allows you to customize the save button text in the report editor when in integrated mode.

The button behavior depends on the configuration:

ScenarioText shown
Without redirectUrl (standard mode)"Save" (default translation)
With redirectUrl and without saveAndFinishText"Save and finish" (default translation)
With redirectUrl and saveAndFinishText definedThe exact value sent in saveAndFinishText
  • Optional: if not provided, the default translation for the user's language is used.
  • Only applies in integrated mode: the parameter only takes effect when there is a redirectUrl defined in the configuration. In standard mode (without redirectUrl) the button always shows "Save" regardless of the value sent.
  • Type: string. The value is inserted as-is, without translation or interpolation, so it must be sent already localized if the integration is multi-language.
  • Typical use case: aligning the button text with the client software's terminology, for example "Send to EHR", "Sign and send", or "Return to clinical history".
{
"accessToken": "{accessToken}",
"redirectUrl": "https://my-software.com/return",
"ui": {
"saveAndFinishText": "Send to EHR"
}
}
Default agent configuration

The defaultAgent object allows you to specify which agent will run automatically:

  • id: Must be a valid identifier of an agent available to the user
  • when: Defines when the agent will run automatically:
    • afterLoad: Immediately after the case is loaded
    • afterStopRecording: After stopping an audio recording

Recommendation: It is advised not to specify this parameter to allow the user to decide which agent to use and when to run it, thus improving the user experience and avoiding unexpected behavior.

Available agents control

The availableAgents parameter allows you to restrict which agents appear in the report editor dropdown. It works with a 3-level filtering system:

  1. Level 1 - Organization agents: All agents the organization has available
  2. Level 2 - User favorite agents: Only the agents the user has marked as favorites
  3. Level 3 - availableAgents: If this array is provided, only the favorite agents included in this array will be shown

Example: If a user has 5 favorite agents (["agent-1", "agent-2", "agent-3", "agent-4", "agent-5"]) and you define availableAgents: ["agent-2", "agent-4"], only agent-2 and agent-4 will appear in the dropdown.

  • Optional: If this parameter is not provided, all the user's favorite agents will be shown (default behavior)
  • Format: Array of strings, where each string is the UUID of an agent
  • Recommendation: Use it when you need to restrict available options based on the integration context or specific client configuration
Telemedicine mode (telemedicine)

The telemedicine object controls the recorder behavior when you also want to capture the audio from the video call (browser tab) in addition to the professional's microphone. It is especially useful for embedded teleconsultation integrations (mini-widget inside the tab that hosts the video call).

{
"accessToken": "{accessToken}",
"ui": {
"telemedicine": {
"captureSurface": "currentTab",
"enabledByDefault": true
}
}
}
PropertyTypeDefaultDescription
captureSurface'currentTab' | 'fullPicker''fullPicker'How capture is offered when starting the recording.
enabledByDefaultbooleanfalseIf true, the recorder starts already in telemedicine mode without any user action required.

captureSurface:

  • 'currentTab': captures the current tab directly via a simplified single-click dialog. Recommended for the embedded mini-widget, where the video call lives in the same tab as the widget.
  • 'fullPicker' (default): shows the browser's full native picker with tabs, windows, and applications, so the user can choose the source (for example, if the video call is in another tab or application).
Browser requirements

Telemedicine mode is only available in desktop Chromium browsers (Google Chrome and Microsoft Edge). In Firefox, Safari, and on mobile devices the button is not shown and the recorder uses only the microphone.

Recommendation
  • Optional: if you do not send the telemedicine object, the recorder works in microphone mode and the user activates telemedicine manually with the corresponding button, preserving their preference between sessions.
  • Reserve enabledByDefault: true for teleconsultation workflows where you always want to capture the video call, preventing the professional from forgetting to enable it.

Base64 Encoding Examples

Base64 encoding is required to send the configuration safely in the URL. Here are examples of how to do it in different languages:

View base64 encoding example in 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}`;
}
View base64 encoding example in 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)
);
}
View base64 encoding example in Python
import base64
import json
from urllib.parse import quote

def generate_redirect_url(case_id: str, access_token: str, redirect_url: str) -> str:
"""Generates the redirect URL to 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)}"
View base64 encoding example in Go
package base64helper

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

// Example usage with the updated configuration
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
}
View base64 encoding example in 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 {
// Convert to JSON, then to base64, and finally URL-encode
byte[] jsonBytes = objectMapper.writeValueAsBytes(config);
String base64 = Base64.getEncoder().encodeToString(jsonBytes);
return URLEncoder.encode(base64, StandardCharsets.UTF_8.toString());
}

// Example usage with the updated configuration
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
);
}
}
View base64 encoding example in C#
using System.Text;
using System.Text.Json;
using System.Web;
using System.Collections.Generic;

public class ConfigEncoder
{
public string EncodeConfigForUrl<T>(T config)
{
// Convert to JSON, then to base64, and finally URL-encode
var json = JsonSerializer.Serialize(config);
var base64 = Convert.ToBase64String(
Encoding.UTF8.GetBytes(json)
);
return HttpUtility.UrlEncode(base64);
}

// Example usage with the updated configuration
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}";
}
}

Event Management

Once the user is on the Llamalitica platform, they can perform various actions that will generate events. These events will be sent to your configured webhook.

Event Format

Each event sent to your webhook will have the following structure:

{
"event": "report.created", // Event type
"organizationId": "org-uuid", // Organization ID
"timestamp": "2024-03-21T10:00:00Z", // Event date and time
"data": {
"id": "report-uuid", // Report ID
"caseId": "case-uuid", // Case ID
"agent": {
"id": "agent-uuid", // ID of the agent used
"name": "Agent Name" // Agent name
},
"content": "Report content", // Generated content
"structuredData": {
// Structured report information
// ... structure according to the selected agent
}
}
}

Event Validation

To ensure the security of your application, it is essential to verify that the events you receive actually come from Llamalitica. This is especially important because webhooks are public endpoints that could be targets of attacks.

How does validation work?

  1. When you configure a webhook, Llamalitica provides you with a unique secret
  2. Each time we send an event to your webhook, we include a signature in the X-Webhook-Signature header
  3. This signature is generated using your secret and the event content
  4. Your application must validate this signature to ensure the event is authentic

Step-by-step example

  1. Get the event signature:

    • The signature comes in the X-Webhook-Signature header
    • It is a hexadecimal string
  2. Generate the expected signature:

    • Take the event body (the complete JSON)
    • Use your webhook secret to create an HMAC SHA-256 hash
    • Convert the result to hexadecimal format
  3. Compare the signatures:

    • If the signatures match, the event is authentic
    • If they do not match, you must reject the event

Implementation examples

View example in TypeScript
import crypto from 'node:crypto';

function validateWebhookSignature(
webhookSecret: string,
payload: unknown,
receivedSignature: string,
): boolean {
// Create an HMAC hash using the secret
const hmac = crypto.createHmac('sha256', webhookSecret);

// Add the payload to the hash
hmac.update(JSON.stringify(payload));

// Generate the expected signature
const expectedSignature = hmac.digest('hex');

// Compare with the received signature
return crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(receivedSignature),
);
}

// Example usage in Express
app.post('/webhook', (req, res) => {
const signature = req.headers['x-webhook-signature'];

if (!signature || typeof signature !== 'string') {
return res.status(401).json({ error: 'Signature not provided' });
}

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

if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}

// Event is valid, process according to its type
const { event, data } = req.body;

switch (event) {
case 'report.created':
// Handle report creation (initially generated by AI)
break;
case 'report.updated':
// Handle report update (saved by the professional)
break;
case 'report.deleted':
// Handle report deletion (removed from timeline)
break;
}

res.json({ received: true });
});
View example in PHP
function validateWebhookSignature(
string $webhookSecret,
string $payload,
string $receivedSignature
): bool {
// Generate the expected signature
$expectedSignature = hash_hmac(
'sha256',
$payload,
$webhookSecret
);

// Compare with the received signature
return hash_equals($expectedSignature, $receivedSignature);
}

// Example usage
$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' => 'Invalid signature']);
exit;
}

// Event is valid, process it
$event = json_decode($payload, true);
// ... process the event
View example in 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:
"""Validates the signature of a webhook event"""
# Generate the expected signature
expected_signature = hmac.new(
webhook_secret.encode(),
payload,
hashlib.sha256
).hexdigest()

# Compare with the received signature
return hmac.compare_digest(
expected_signature,
received_signature
)

@app.post("/webhook")
async def webhook(request: Request):
# Get the signature from the header
signature = request.headers.get("x-webhook-signature")
if not signature:
raise HTTPException(401, "Signature not provided")

# Read the payload as bytes
payload = await request.body()

# Validate the signature
if not validate_webhook_signature(
webhook_secret="your-secret-here",
payload=payload,
received_signature=signature
):
raise HTTPException(401, "Invalid signature")

# Event is valid, process it
event = await request.json()
# ... process the event

return {"received": True}
Best practices
  • Use timing-safe comparison to protect against timing attacks (such as crypto.timingSafeEqual or hash_equals)
  • Store the webhook secret securely (environment variables, secrets manager, etc.)
  • Reject events without a signature or with an invalid signature

Security Considerations

  • API Key:

    • Store the API Key securely in environment variables
    • Never expose it in the frontend or in public code
    • Rotate the key periodically or if you suspect it has been compromised
  • Webhooks:

    • Always validate the signature of received events
    • Use HTTPS for your webhook endpoint
  • User Tokens:

    • Tokens are temporary and specific to each session

Frequently Asked Questions

How do I get the required credentials?

  1. Request developer access from our website
  2. Once your request is approved, you will have access to the developer section
  3. In the developer panel you can:
    • Generate and manage your API Keys
    • Configure webhooks
    • Access the complete documentation
  4. Store your credentials securely

Can I customize the Llamalitica interface?

Yes, through the UI configuration object you can customize several aspects:

  • Component visibility
  • Theme colors
  • Default template
  • Available features
  • Recorder telemedicine mode (video call capture)

What happens if the token expires?

The generated token has a sufficient expiry time for the user to reach the Llamalitica page. Once on the platform, you do not need to worry about token renewal, as the web application itself manages and renews tokens automatically to keep the session active.

How do I handle errors?

Implement error handling for API calls and verify HTTP status codes:

  • 401: Invalid or expired token
  • 402: Insufficient credits
  • 404: Resource not found
  • 500: Internal server error

Support

For any questions or issues during the integration, you can: