# Data Structure Documentation
Source: https://docs.lovi.ai/en/api-reference/api_data_en
Official documentation for extracting raw data from the database
# Official documentation for extracting raw data from the database
This document aims to provide access and a description of the main data structures available to the company to facilitate proper integration, management, and analysis of information related to interactions and participating agents.
In addition to describing each table structure, it includes recommendations and warnings regarding the responsible handling of personal data that may be found in the records, highlighting the importance of complying with current privacy and information security regulations.
We suggest carefully reviewing each field definition and the notes on handling personal data before starting any integration or analysis process to ensure the safe and proper use of the provided database.
The data presented is raw and unprocessed.
## Accessing the Data in ClickHouse via HTTP and BI Tools
This section explains how to connect to the Botslovers ClickHouse database, either through direct HTTP access (for example using `curl`) or via graphical tools and Business Intelligence tools like DBeaver, Power BI, or Tableau.
### Access via HTTP (example using curl)
You can access the data by executing SQL queries directly against the ClickHouse database via its HTTP interface.
**Basic Example:**
To retrieve the first 10 records from the `messages_` table:
```bash theme={null}
curl -u user:'password' 'https://public-clickhouse.botslovers.com/?query=SELECT%20*%20FROM%20messages_company%20LIMIT%2010'
```
Replace `user` and `password` with your personal credentials.
The `query` parameter must be URL-encoded.
**Response Formats:**
By default, the response is in TabSeparated format. You can request other formats by adding `FORMAT` at the end of the query:
**JSON Example:**
```bash theme={null}
curl -u user:'password' "https://public-clickhouse.botslovers.com/?query=SELECT%20*%20FROM%20messages_company%20LIMIT%2010%20FORMAT%20JSON"
```
**CSV Example:**
```bash theme={null}
curl -u user:'password' "https://public-clickhouse.botslovers.com/?query=SELECT%20*%20FROM%20messages_company%20LIMIT%2010%20FORMAT%20CSV"
```
### Access from BI Tools or Database Clients (DBeaver, Power BI, Tableau, etc.)
ClickHouse supports JDBC and ODBC connectors, allowing you to use graphical tools to explore and analyze the data.
#### Access via DBeaver
* Open DBeaver and create a new connection.
* Select ClickHouse as the database type.
* Choose HTTP as the connection type.
* Fill in the following fields:
* Host: `public-clickhouse.botslovers.com`
* Port: `443`
* User: your username
* Password: your password
* SSL: enabled (make sure the box is checked)
* Test the connection and save.
#### Access from BI Tools (Power BI, Tableau, etc.)
**Power BI**
* Download and install the official ClickHouse ODBC driver.
* Configure an ODBC DSN pointing to:
* Host: `public-clickhouse.botslovers.com`
* Port: `443` (or `8443` depending on configuration, check with your administrator)
* SSL Mode: enabled
* Username/Password: your credentials
* In Power BI, create a new ODBC data source and select the configured DSN.
**Tableau**
* You can use the ClickHouse ODBC driver as in Power BI.
* Alternatively, use the native ClickHouse connector.
## Security and Best Practices
* Your credentials are personal and must not be shared.
* Consult your administrator if you have questions about usage limits or data structure.
* We recommend avoiding queries that extract large volumes of data to prevent potential service performance issues.
## Tables
### `messages`
Acts as the central repository where each individual message generated in a conversation is stored. This document details its fields, purpose, and specific recommendations for managing sensitive data securely.
### `agent_activity`
Tables that concentrate relevant information about agents — human or automated — participating in conversations. They facilitate associating events and metrics to each agent for individual activity analysis, key indicator calculations (e.g., response times, session closures, agent availability), and action traceability across conversations, even when transfers occur.
## Table Descriptions
### `messages` (Table: `messages_`)
Stores every message exchanged during conversations.
| Field | Type | Description |
| ------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `sender_id` | String | Unique sender identifier linking each message to a specific entity. |
| `sender_type` | String | Message origin: `"user"`, `"bot"`, `"system"`, or `"human"`. |
| `id` | String | Unique key of each message, ensuring duplicate detection. |
| `created_at` | DateTime | Timestamp in UTC for when the message was generated. |
| `channel` | String | Channel through which the message was sent: `"whatsapp"` or `"web"` (widget). |
| `session` | String | Conversation identifier grouping multiple messages. |
| `direction` | String | Message direction: `"inbound"` or `"outbound"`. |
| `type` | String | Communicative type: `"inbound"` (received) or `"outbound"` (sent). |
| `text` | String | Full free-text content of the message. May include text, emojis, emails, IDs, etc. **Note:** May be `null` for images or documents. |
| `language` | String | Language code (e.g., `"es"`, `"en"`). **Note:** May be `null` if undetected. |
### `agent_activity` (Table: `chat_agent_log_`)
Records events occurring during agents' interaction with chats.
| Field | Type | Description |
| ----------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id` | String | Unique event identifier. |
| `chat_id` | String | Associated chat identifier. |
| `agent_id` | String | Unique agent identifier involved in the event. |
| `joined_at` | DateTime | UTC timestamp when the agent joined the chat. |
| `left_at` | Nullable(DateTime) | UTC timestamp when the agent left the chat, or `null` if still active. |
| `event` | String | Type of event. Possible values: `bot_assigned`, `agent_timeout`, `user_timeout`, `agent_joined`, `chat_escalated`, `manual_close`, `resolved`, `assigned_to_human`, `transferred`. |
| `response_rating` | Nullable(Int16) | Rating given for the interaction, if any. |
| `created_at` | DateTime | Record creation timestamp. |
| `updated_at` | DateTime | Record last update timestamp. |
| `deleted_at` | Nullable(DateTime) | Logical deletion timestamp, if applicable. |
### `agent_status_logs` (Table: `agent_status_logs_`)
Tracks the history of agent status changes over time, such as transitioning from "online" to "break".
| Field | Type | Description |
| ------------------- | --------------- | -------------------------------------------------------------------------------------------- |
| `id` | String | Unique status change identifier. |
| `agent_id` | String | Unique agent identifier. |
| `old_status` | String | Previous agent status: `training`, `offline`, `break`, `ending_shift`, `inactive`, `online`. |
| `new_status` | String | New agent status: `training`, `offline`, `break`, `ending_shift`, `inactive`, `online`. |
| `status_changed_at` | DateTime | UTC timestamp when the status changed. |
| `duration_seconds` | Nullable(Int64) | Duration in seconds of the previous status, or `null` if not applicable. |
| `created_at` | DateTime | Record creation timestamp. |
## Handling of Personal Data
The `messages_`table, especially the `text` column, retains the full content of customer interactions, potentially containing personal data such as names, email addresses, ID numbers, or other details about the products or services offered by your company.
These records have not been anonymized or pseudonymized to preserve the full context of each conversation and ensure analysis accuracy.
It is **essential** to process this data according to applicable regulations, always maintaining confidentiality, security, and the rights of data subjects.
# WhatsApp Notifications API
Source: https://docs.lovi.ai/en/api-reference/api_push_whatsapp_en
Official documentation for using Lovi's API with WhatsApp via Postman
## Introduction
This section provides the official documentation for using Lovi's API with WhatsApp via Postman. It includes detailed instructions on how to set up and test API requests for WhatsApp integration, ensuring smooth communication through the platform. Authentication is performed using tokens that enable basic authentication for the API services. For more details on how to authenticate, please refer to the [Authentication](./authentication) page.
The Lovi API supports WhatsApp notifications with multimedia content, dynamic placeholders, scheduled delivery, and conversation flow integration.
### Key Features:
* WhatsApp notifications with multimedia support
* Dynamic content personalization with placeholders
* Scheduled message delivery with timezone support
* Conversation flow integration
* Two data structure formats (nested and flat)
* Campaign status via `inquiry_set_id` and `GET /notify/status` (same endpoint for voice campaigns)
***
## 📣 Send WhatsApp Notification
To send a notification via the Lovi API, make a POST request to the endpoint with the necessary parameters and authentication.
> **Method**: POST **Format**: JSON
### Endpoint
```
POST https://cloud.lovi.ai/functions/v1/notify?access_key={YOUR_ACCESS_KEY}
```
### Query Parameters
| Parameter | Required | Description |
| ------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `access_key` | Yes | Your unique API access key. |
| `unflatten` | No | If set to `true`, the body must contain flat variables (no nested objects). If set to `false` or omitted, the body can contain nested objects. |
**Example URLs:**
```
POST https://cloud.lovi.ai/functions/v1/notify?access_key=your-api-key
POST https://cloud.lovi.ai/functions/v1/notify?access_key=your-api-key&unflatten=true
```
### Headers
| Key | Value | Required | Description |
| ------------ | ---------------- | -------- | -------------------------------------------------- |
| Content-Type | application/json | Yes | Indicates that the request body is in JSON format. |
**Note**: Authentication is handled via the `access_key` parameter in the URL, not through headers.
***
## 📋 Request Parameters
The API supports two data structure formats controlled by the `unflatten` parameter.
### Required Parameters
| Parameter | Type | Description | Example |
| ----------------------- | --------------- | ---------------------------------------------------------------------------- | --------------------------------------------- |
| `contact` OR `contacts` | Object OR Array | Single number contact (object) OR Multiple numbers contacts (array, max 100) | See below |
| `language_template` | String | Language code for the template | `"es_ES"`, `"en_US"`, `"es"`, `"es"` |
| `name_template` | String | Name of the approved template | `"welcome_user"` |
| `recipient_id` | String | Sender's ID number or phone number | `"34666033135"` |
| `notification_type` | String | Type for analytics | `"marketing"`, `"transactional"`, `"utility"` |
| `campaign_name` | String | Campaign identifier name | `"Black Friday 2024"` |
**Important**: You must use either `contact` (for single recipient) OR `contacts` (for multiple recipients), but NOT both.
### Optional Parameters
| Parameter | Type | Description | When to Use | Example |
| ------------------- | -------- | ----------------------------------- | --------------------------------------------------- | ----------------------- |
| `contact.name` | String | Contact's name for personalization | For personalized messages | `"Juan Pérez"` |
| `contact.email` | String | Contact's email | For additional contact data | `"juan@email.com"` |
| `name_event` | String | Event to trigger conversation flows | When you want to start a specific conversation flow | `"intent-general-push"` |
| `datetime_sending` | DateTime | Scheduled delivery time (ISO 8601) | When you want to schedule the message for later | `"2024-12-25T10:30:00"` |
| `timezone` | String | Timezone for scheduled delivery | When using `datetime_sending` | `"Europe/Madrid"` |
| `components_push.*` | Mixed | Template components (text, media) | When template requires dynamic content | See components section |
***
## 👥 Single vs Multiple Recipients
### Using `contact` - Send to One Person
Use `contact` when you want to send a notification to **one recipient**.
**Structure**:
```json theme={null}
{
"contact": {
"number": "34666033135",
"name": "John",
"email": "john@example.com"
}
}
```
* `contact` is an **object** (not a list)
* Required field: `number`
* Optional fields: `name`, `email`, and any custom fields
***
### Using `contacts` - Send to Multiple People (Bulk Sending)
Use `contacts` when you want to send the same notification to **multiple recipients** at once.
**Structure**:
```json theme={null}
{
"contacts": [
{
"number": "34666033135",
"name": "John"
},
{
"number": "34777044246",
"name": "Sarah"
},
{
"number": "34888055357",
"name": "Michael"
}
]
}
```
* `contacts` is a **list/array** (not a single object)
* **Maximum**: 100 contacts per request
* Each contact in the list must have a `number`
* Optional fields: `name`, `email`, and any custom fields
**Important Restrictions**:
* ⚠️ **Cannot use `unflatten=true` with `contacts`** - Bulk sending only works with nested structure
* ⚠️ **Cannot use both `contact` and `contacts`** in the same request - choose one
* ⚠️ **`contacts` list cannot be empty** - must have at least 1 contact
***
## 🔄 Data Structure Formats
The API supports two formats based on the `unflatten` parameter:
### Nested Structure (unflatten=false or omitted)
When `unflatten=false` or not specified, use nested objects:
```json theme={null}
{
"contact": {
"number": "34666033135",
"name": "Juan Pérez",
"email": "juan@email.com"
},
"components_push": {
"header_text_0": "Welcome Message",
"body_text_0": "Hello {{name}}",
"body_text_1": "Your order is ready!",
"footer_text_0": "Support Team"
},
"language_template": "es_ES",
"name_template": "order_ready",
"recipient_id": "34666033135",
"notification_type": "transactional",
"campaign_name": "Order Notifications"
}
```
### Flat Structure (unflatten=true)
When `unflatten=true`, all nested objects must be flattened using dot notation:
```json theme={null}
{
"contact.number": "34666033135",
"contact.name": "Juan Pérez",
"contact.email": "juan@email.com",
"components_push.header_text_0": "Welcome Message",
"components_push.body_text_0": "Hello {{name}}",
"components_push.body_text_1": "Your order is ready!",
"components_push.footer_text_0": "Support Team",
"language_template": "es_ES",
"name_template": "order_ready",
"recipient_id": "34666033135",
"notification_type": "transactional",
"campaign_name": "Order Notifications"
}
```
### When to Use Each Format
* **Nested Structure (`unflatten=false`)**: Recommended for better readability and when your system supports nested objects
* **Flat Structure (`unflatten=true`)**: Use when your system doesn't support nested objects or requires flat data structure
***
## 🎨 Components & Multimedia
**IMPORTANT**: Components you can send dynamically are **ONLY those returned by the template components endpoint**. The structure varies depending on whether the template has variables or media.
### Component Naming Rules
* **Header media**: `header_image`, `header_video`, `header_document` (NO suffix number)
* **Body variables**: `body_text_0`, `body_text_1`, `body_text_2`, etc. (with index number for each `{"{1}"}`, `{"{2}"}`, `{"{3}"}` placeholder)
* **Footer and Buttons**: Are STATIC in the template definition and CANNOT be sent dynamically
### How to Know Which Components to Send
1. First, call the template components endpoint:
```
GET /notify/template/components?access_key={key}&template={template_id}
```
2. The API returns only the components you need to provide:
```json theme={null}
{
"status": "OK",
"components": ["header_image", "body_text_0", "body_text_1"]
}
```
3. Send ONLY those components in your notification request
### Component Types by Position
#### Header Components (Media Only)
**IMPORTANT**: Only ONE header component per template. Header text is STATIC in the template.
| Type | Component Name | Description | Example Value | Specifications |
| ---------- | ----------------- | ------------------- | --------------------------------- | ------------------------------ |
| `image` | `header_image` | Header image URL | `"https://example.com/image.jpg"` | JPG, PNG, GIF - Max 5MB |
| `video` | `header_video` | Header video URL | `"https://example.com/video.mp4"` | MP4, AVI, MOV - Max 16MB, 30s |
| `document` | `header_document` | Header document URL | `"https://example.com/doc.pdf"` | PDF, DOC, XLS, PPT - Max 100MB |
**Note**: `header_text` is NOT a dynamic component. Header text is defined in the template and cannot be changed.
#### Body Components (Variables Only)
Body text with placeholders requires variables in order: `{"{1}"}`, `{"{2}"}`, `{"{3}"}`, etc.
| Component Name | Description | Maps to Template Placeholder |
| -------------- | ----------------------- | ---------------------------- |
| `body_text_0` | First variable in body | `{"{1}"}` |
| `body_text_1` | Second variable in body | `{"{2}"}` |
| `body_text_2` | Third variable in body | `{"{3}"}` |
| `body_text_N` | N-th variable in body | `{"{N}"}` |
**Example Template**: "Hello `{"{1}"}`, your course `{"{2}"}` is ready"
* `body_text_0`: Value for `{"{1}"}` (e.g., "Maria")
* `body_text_1`: Value for `{"{2}"}` (e.g., "JavaScript")
#### Footer Components
⚠️ **Footer is STATIC** - defined in the template and cannot be modified per message.
#### Button Components
**IMPORTANT**: Most buttons are STATIC in the template. However, URL buttons with variables CAN be dynamic.
| Component Name | Description | When It Appears |
| --------------- | ------------------------------------------------- | ------------------------------------------------------- |
| `buttons_url_0` | URL for first URL button with variable `{"{1}"}` | When template has URL button with `{"{1}"}` placeholder |
| `buttons_url_1` | URL for second URL button with variable `{"{2}"}` | When template has multiple URL buttons with variables |
**Example Template Button**: `"url": "https://example.com/{"{1}"}"`
* The template defines the URL pattern with placeholder
* You provide the value for `{"{1}"}` via `buttons_url_0`
**Note**: Quick reply buttons are always static and cannot be modified per message.
### Component Examples
#### Example 1: Template with Image Header (Nested Structure)
```json theme={null}
{
"components_push": {
"header_image": "https://cdn.example.com/promo.jpg"
}
}
```
#### Example 2: Template with Body Variables (Nested Structure)
```json theme={null}
{
"components_push": {
"body_text_0": "María",
"body_text_1": "JavaScript Advanced Course"
}
}
```
#### Example 3: Template with Image and Variables (Flat Structure)
```json theme={null}
{
"components_push.header_image": "https://cdn.example.com/promo.jpg",
"components_push.body_text_0": "María",
"components_push.body_text_1": "JavaScript Advanced Course"
}
```
***
## 🧩 Template Variables (Body Text)
**CRITICAL**: Variables in WhatsApp templates use **positional placeholders** like `{"{1}"}`, `{"{2}"}`, `{"{3}"}`, NOT named variables like `{"{name}"}`.
### How Template Variables Work
WhatsApp templates define variables as numbered placeholders in the template body text:
* Template text: "Hello `{"{1}"}`, your course `{"{2}"}` is ready"
* `{"{1}"}` maps to `body_text_0`
* `{"{2}"}` maps to `body_text_1`
### Variable Assignment
You provide the **values** for these numbered placeholders in the `components_push` object:
```json theme={null}
{
"components_push": {
"body_text_0": "María",
"body_text_1": "JavaScript Advanced Course"
}
}
```
**Result**: "Hello María, your course JavaScript Advanced Course is ready"
### Important Rules
1. **Positional order matters**: `body_text_0` = `{"{1}"}`, `body_text_1` = `{"{2}"}`, etc.
2. **Direct values**: Provide the actual value, not `{"{variable}"}` syntax
3. **All variables required**: Must provide values for all `{"{N}"}` placeholders in the template
4. **No mixing**: Cannot combine variables with static text in components\_push
### ✅ Valid Examples
**Template**: "Hello `{"{1}"}`, your order `{"{2}"}` is ready"
```json theme={null}
{
"contact": {
"name": "María",
"order_id": "ORD-12345"
},
"components_push": {
"body_text_0": "María",
"body_text_1": "ORD-12345"
}
}
```
**You can also use contact fields directly**:
```json theme={null}
{
"contact": {
"name": "María",
"order_id": "ORD-12345"
},
"components_push": {
"body_text_0": "{{name}}",
"body_text_1": "{{order_id}}"
}
}
```
### ❌ Invalid Examples
```json theme={null}
{
"components_push": {
"body_text_0": "Hello María", // ❌ Don't add template text, only the value
"body_text_1": "{1}" // ❌ Don't use {1}, provide the actual value
}
}
```
### Variable Resolution
The system resolves `{"{variable}"}` references in `components_push` by searching:
1. First in `contact` object
2. Then in root-level parameters
```json theme={null}
{
"contact": {
"name": "María",
"email": "maria@example.com"
},
"order_id": "ORD-12345",
"components_push": {
"body_text_0": "{{name}}", // Resolved from contact.name → "María"
"body_text_1": "{{order_id}}" // Resolved from root order_id → "ORD-12345"
}
}
```
***
## ⏰ Scheduling & Conversation Flows
### Immediate Delivery (Default)
If `datetime_sending` is not specified, the message is sent immediately:
```json theme={null}
{
"contact": { "number": "34666033135" },
"language_template": "es_ES",
"name_template": "welcome_user",
"recipient_id": "34666033135",
"notification_type": "marketing",
"campaign_name": "Welcome Campaign"
}
```
### Scheduled Delivery
Use `datetime_sending` and `timezone` to schedule messages:
**Nested Structure:**
```json theme={null}
{
"contact": { "number": "34666033135" },
"language_template": "es_ES",
"name_template": "welcome_user",
"recipient_id": "34666033135",
"notification_type": "marketing",
"campaign_name": "Welcome Campaign",
"datetime_sending": "2024-12-25T10:30:00",
"timezone": "Europe/Madrid"
}
```
**Flat Structure:**
```json theme={null}
{
"contact.number": "34666033135",
"language_template": "es_ES",
"name_template": "welcome_user",
"recipient_id": "34666033135",
"notification_type": "marketing",
"campaign_name": "Welcome Campaign",
"datetime_sending": "2024-12-25T10:30:00",
"timezone": "Europe/Madrid"
}
```
### Conversation Flow Integration
Use `name_event` to trigger specific conversation flows:
```json theme={null}
{
"contact": { "number": "34666033135" },
"language_template": "es_ES",
"name_template": "course_info",
"recipient_id": "34666033135",
"notification_type": "marketing",
"campaign_name": "Course Enrollment",
"name_event": "info-course-nursing"
}
```
***
## 📋 Complete Examples
### Example 1: Single Recipient with Body Variables
**Sending to ONE person using `contact`**
**Template**: "Hello `{"{1}"}`, thank you for your interest in `{"{2}"}`!"
**Request:**
```json theme={null}
{
"contact": {
"number": "34666033135",
"customer_name": "Sarah"
},
"components_push": {
"body_text_0": "{{customer_name}}",
"body_text_1": "our Premium Membership"
},
"language_template": "en",
"name_template": "interest_confirmation",
"recipient_id": "34666033135",
"notification_type": "marketing",
"campaign_name": "Membership Campaign 2024"
}
```
**Response:**
```json theme={null}
{
"message": "Registration successful",
"inquiry_set_id": "d891b9b4-2f79-45a0-bc99-9a2922d4f713"
}
```
***
### Example 1B: Multiple Recipients (Bulk Sending)
**Sending to MULTIPLE people using `contacts`**
**Template**: "Hello `{"{1}"}`, thank you for your interest in `{"{2}"}`!"
**Request:**
```json theme={null}
{
"contacts": [
{
"number": "34666033135",
"customer_name": "Sarah"
},
{
"number": "34777044246",
"customer_name": "John"
},
{
"number": "34888055357",
"customer_name": "Michael"
}
],
"components_push": {
"body_text_0": "{{customer_name}}",
"body_text_1": "our Premium Membership"
},
"language_template": "en",
"name_template": "interest_confirmation",
"recipient_id": "bulk_campaign",
"notification_type": "marketing",
"campaign_name": "Membership Campaign 2024"
}
```
**Response:**
```json theme={null}
{
"success": true,
"message": "Processing completed",
"total": 3,
"processed": 3,
"failed": 0,
"errors": []
}
```
**If some contacts fail:**
```json theme={null}
{
"success": true,
"message": "Processing completed",
"total": 3,
"processed": 2,
"failed": 1,
"errors": [
{
"contact": { "number": "34888055357", "customer_name": "Michael" },
"reason": "Invalid phone number format"
}
]
}
```
***
### Example 2: Template with Image Header
**Template components**: `["header_image"]`
**URL:**
```
POST https://cloud.lovi.ai/functions/v1/notify?access_key=your-api-key
```
**Nested Structure:**
```json theme={null}
{
"contact": {
"number": "34666033135",
"customer_name": "Robert",
"email": "robert@example.com"
},
"components_push": {
"header_image": "https://cdn.example.com/seasonal-promotion.jpg"
},
"language_template": "en",
"name_template": "seasonal_offer",
"recipient_id": "34666033135",
"notification_type": "marketing",
"campaign_name": "Holiday Sale 2024",
"datetime_sending": "2024-12-24T09:00:00",
"timezone": "Europe/Madrid"
}
```
**Note**: Body, footer, and buttons are static in the template and cannot be modified.
### Example 3: Template with PDF Document
**Template components**: `["header_document"]`
**URL:**
```
POST https://cloud.lovi.ai/functions/v1/notify?access_key=your-api-key&unflatten=true
```
**Flat Structure:**
```json theme={null}
{
"contact.number": "34666033135",
"contact.customer_name": "Jennifer",
"contact.email": "jennifer@example.com",
"components_push.header_document": "https://files.example.com/statement-2024.pdf",
"language_template": "en",
"name_template": "document_delivery",
"recipient_id": "34666033135",
"notification_type": "transactional",
"campaign_name": "Monthly Statements"
}
```
### Example 4: Conversation Flow Trigger
**Nested Structure:**
```json theme={null}
{
"contact": {
"number": "34666033135",
"first_name": "Laura",
"last_name": "Martínez",
"course_interest": "Web Development"
},
"language_template": "en_US",
"name_template": "course_inquiry",
"recipient_id": "34666033135",
"notification_type": "marketing",
"campaign_name": "Course Enrollment Q1",
"name_event": "course-web-development-info",
"datetime_sending": "2024-12-26T10:00:00",
"timezone": "Europe/Madrid"
}
```
***
## 📊 Response Codes
### Successful Response (200 OK)
Single `contact`:
```json theme={null}
{
"message": "Registration successful",
"inquiry_set_id": "d891b9b4-2f79-45a0-bc99-9a2922d4f713"
}
```
Array `contacts` (up to 100): `message` is `"Processing completed"` plus `total`, `processed`, `failed`, and `errors`. There is no `notification_id` on this API.
Authentication is the `access_key` query parameter (no `Authorization` header). **Create** requires `whatsapp_notifications` on the key. **Status** does not: any valid key for the same company can query.
### Error Responses
Error bodies are `{ "success": false, "message": "…" }`.
#### 400 Bad Request
Missing `access_key`, invalid body, or (on status) missing `campaign_id`.
```json theme={null}
{
"success": false,
"message": "Required field missing"
}
```
#### 403 Forbidden — Create without permission
The key lacks `whatsapp_notifications` (or `voice_notifications` on `POST /notify/voice`).
```json theme={null}
{
"success": false,
"message": "Forbidden"
}
```
#### 404 Not Found — Invalid key or missing campaign
An invalid `access_key` returns **404**, not 401 (`Invalid access_key`). The same 404 is used when the campaign is missing or belongs to another company (`Campaign not found`).
```json theme={null}
{
"success": false,
"message": "Invalid access_key"
}
```
#### 500 Internal Server Error
```json theme={null}
{
"success": false,
"message": "Request failed"
}
```
***
## Query campaign status
After you create a campaign, poll progress with `GET`. Pass `campaign_id` = the `inquiry_set_id` from the create response. **The same status endpoint covers WhatsApp and voice** (`POST /notify/voice`).
### Endpoint
Production:
```http theme={null}
GET https://cloud.lovi.ai/functions/v1/notify/status?access_key={YOUR_ACCESS_KEY}&campaign_id={CAMPAIGN_ID}
```
Verified host: `https://cloud.lovi.ai/functions/v1/…`. `https://app.lovi.ai/functions/...` is not this API (it serves the frontend). `app-dev.lovi.ai` is not documented here as a functions host.
Keep `access_key` secret. Examples use placeholders only.
### Query parameters
| Parameter | Required | Description |
| ------------- | -------- | ------------------------------------------- |
| `access_key` | Yes | Valid key. Only campaigns for that company. |
| `campaign_id` | Yes | UUID = `inquiry_set_id` from create. |
### Successful response example
```json theme={null}
{
"campaign_id": "d891b9b4-2f79-45a0-bc99-9a2922d4f713",
"name": "Membership Campaign 2026",
"status": "processing",
"general_failure": false,
"alert_message": null,
"created_at": "2026-08-10T09:30:00Z",
"total_recipients": 100,
"sent": 76,
"failed": 4,
"pending": 20,
"responded": 18,
"by_status": {
"delivered": 61,
"read": 15,
"failed": 4,
"pending": 20
}
}
```
### Field meanings
| Field | Description |
| ------------------ | -------------------------------------------------------------------------------------------------------- |
| `campaign_id` | Campaign being queried. |
| `name` | Campaign name. |
| `status` | Observed values: `pending`, `processing`, `ready`, `completed`, `failed` (varchar, not a Postgres ENUM). |
| `general_failure` | `true` when overall status is `failed`. |
| `alert_message` | Alert text, if any. |
| `created_at` | Creation time. |
| `total_recipients` | Registered recipients. |
| `sent` | Recipients in `delivered`, `read`, `sent`, `completed`, or `done`. |
| `failed` | Recipients in `failed`, `warning`, `busy`, `no-answer`, or `voicemail`. |
| `pending` | Everyone else. |
| `responded` | Distinct chats for the campaign with at least one user message. |
| `by_status` | Counts by original recipient status. |
Counters can change while sending is in progress. No HTTP rate limit and no pagination on this endpoint.
### Possible errors
| Code | Cause |
| ----- | --------------------------------------------------------------------------------- |
| `400` | Missing `campaign_id` or `access_key`. |
| `404` | `Invalid access_key`, or campaign missing / other company (`Campaign not found`). |
| `500` | Status lookup failed. |
***
***
## 🔧 Best Practices
### Data Structure
* **Prefer nested structure** (`unflatten=false`) for better readability
* **Use flat structure** (`unflatten=true`) only when your system requires it
* **Validate structure** before sending requests
### Media Guidelines
* **Use HTTPS URLs** for all media files
* **Optimize file sizes** for faster delivery
* **Use public URLs** without authentication requirements
* **Test media URLs** before sending to ensure accessibility
### Scheduling
* **Specify timezone** when using `datetime_sending`
* **Validate future dates** before scheduling
* **Consider business hours** for better engagement
* **Test scheduling** in development environment
### Placeholders
* **Use meaningful variable names** that match your data
* **Test variable substitution** before production
* **Keep one variable per field** to avoid errors
* **Provide fallback values** in your application logic
### Performance
* **Batch multiple notifications** when possible
* **Cache template information** to reduce API calls
* **Monitor rate limits** and implement backoff strategies
* **Use connection pooling** for better performance
***
## 🚨 Common Errors & Solutions
### Contact/Contacts Validation Errors
#### Error: Missing Contact Information
**Problem**: Request doesn't include `contact` or `contacts`
```json theme={null}
{
"error": "validation_failed",
"message": "Missing required field: 'contact' or 'contacts'"
}
```
**Solution**: You must include either `contact` (for one person) OR `contacts` (for multiple people)
***
#### Error: Using Both Contact and Contacts
**Problem**: Request includes both `contact` and `contacts` at the same time
```json theme={null}
{
"error": "validation_failed",
"message": "Cannot use both 'contact' and 'contacts' simultaneously"
}
```
**Solution**: Choose only one - use `contact` for single recipient or `contacts` for multiple recipients
***
#### Error: Contact is a List Instead of Object
**Problem**: Sent `contact` as a list `[...]` instead of an object `{...}`
```json theme={null}
{
"error": "validation_failed",
"message": "'contact' must be an object, not an array"
}
```
**Solution**: For single recipient, use `contact` as an object:
```json theme={null}
{
"contact": {
"number": "34666033135",
"name": "John"
}
}
```
***
#### Error: Contacts is Not a List
**Problem**: Sent `contacts` as an object `{...}` instead of a list `[...]`
```json theme={null}
{
"error": "validation_failed",
"message": "'contacts' must be an array, not an object"
}
```
**Solution**: For multiple recipients, use `contacts` as a list:
```json theme={null}
{
"contacts": [
{ "number": "34666033135", "name": "John" },
{ "number": "34777044246", "name": "Sarah" }
]
}
```
***
#### Error: Empty Contacts List
**Problem**: Sent `contacts` as an empty list `[]`
```json theme={null}
{
"error": "validation_failed",
"message": "'contacts' array cannot be empty"
}
```
**Solution**: Include at least one contact in the list
***
#### Error: Too Many Contacts
**Problem**: Sent more than 100 contacts in the `contacts` list
```json theme={null}
{
"error": "validation_failed",
"message": "Maximum 100 contacts allowed per request. Received: 150"
}
```
**Solution**: Split your contacts into multiple requests of maximum 100 contacts each
***
#### Error: Unflatten with Contacts
**Problem**: Used `unflatten=true` with `contacts` (bulk sending)
```json theme={null}
{
"error": "validation_failed",
"message": "Parameter 'unflatten' is incompatible with 'contacts' array"
}
```
**Solution**: Bulk sending with `contacts` only works with nested structure. Remove `unflatten=true` or use `unflatten=false`
***
### Structure Mismatch
**Problem**: Mixing nested and flat structures
**Solution**: Choose one format consistently based on `unflatten` parameter
### Template Not Found
**Problem**: Using non-existent or non-approved template
**Solution**: Verify template name and approval status using template management endpoints
### Invalid Phone Format
**Problem**: Including '+' or spaces in phone number
**Solution**: Use clean international format without symbols (e.g., `34666033135`)
### Component Naming Errors
**Problem**: Using incorrect component names like `header_image_0` instead of `header_image`
**Solution**: Always check template components first using the `/notify/template/components` endpoint
### Variable Position Errors
**Problem**: Incorrect mapping between template placeholders and body\_text components
**Solution**: Remember `{"{1}"}` = `body_text_0`, `{"{2}"}` = `body_text_1`, etc.
### Modifying Static Components
**Problem**: Trying to send footer, buttons, or header text dynamically
**Solution**: These components are static in the template. Only send components returned by the template components endpoint
### Scheduling Errors
**Problem**: Past dates or invalid timezone
**Solution**: Use future dates in ISO 8601 format with valid IANA timezone codes
***
## 🔬 Template Examples
These examples demonstrate different types of template structures.
### Example 1: Simple Text Only Template
**Components returned by API**: `[]` (empty - no dynamic components)
```json theme={null}
{
"contact": {
"number": "34666033135"
},
"language_template": "en",
"name_template": "welcome_message",
"recipient_id": "34666033135",
"notification_type": "marketing",
"campaign_name": "Welcome Campaign"
}
```
**Note**: No `components_push` needed since template has no dynamic components.
***
### Example 2: Template with Body Variables
**Template Body**: "Hello `{"{1}"}`, your `{"{2}"}` is ready!"
**Components returned by API**: `["body_text_0", "body_text_1"]`
```json theme={null}
{
"contact": {
"number": "34666033135",
"customer_name": "John",
"product_name": "Premium Package"
},
"components_push": {
"body_text_0": "{{customer_name}}",
"body_text_1": "{{product_name}}"
},
"language_template": "en",
"name_template": "order_ready",
"recipient_id": "34666033135",
"notification_type": "transactional",
"campaign_name": "Order Notifications"
}
```
**Result**: "Hello John, your Premium Package is ready!"
***
### Example 3: Template with Image Header
**Components returned by API**: `["header_image"]`
```json theme={null}
{
"contact": {
"number": "34666033135"
},
"components_push": {
"header_image": "https://example.com/promotional-banner.jpg"
},
"language_template": "en",
"name_template": "promo_with_image",
"recipient_id": "34666033135",
"notification_type": "marketing",
"campaign_name": "Holiday Sale"
}
```
**Note**: Body, footer, and buttons are static in the template.
***
### Example 4: Template with Video Header
**Components returned by API**: `["header_video"]`
```json theme={null}
{
"contact": {
"number": "34666033135"
},
"components_push": {
"header_video": "https://example.com/product-demo.mp4"
},
"language_template": "en",
"name_template": "video_announcement",
"recipient_id": "34666033135",
"notification_type": "marketing",
"campaign_name": "Product Launch"
}
```
***
### Example 5: Template with PDF Document
**Components returned by API**: `["header_document"]`
```json theme={null}
{
"contact": {
"number": "34666033135",
"customer_name": "Jane Smith"
},
"components_push": {
"header_document": "https://example.com/invoice-12345.pdf"
},
"language_template": "en",
"name_template": "invoice_delivery",
"recipient_id": "34666033135",
"notification_type": "transactional",
"campaign_name": "Monthly Invoices"
}
```
***
### Example 6: Template with Header Text and Body Variable
**Header**: "Important Notice" (static text in template)
**Body**: "Hello `{"{1}"}`, welcome to our platform!"
**Components returned by API**: `["body_text_0"]`
```json theme={null}
{
"contact": {
"number": "34666033135",
"first_name": "Michael"
},
"components_push": {
"body_text_0": "{{first_name}}"
},
"language_template": "en",
"name_template": "welcome_with_header",
"recipient_id": "34666033135",
"notification_type": "utility",
"campaign_name": "User Onboarding"
}
```
**Result**:
* Header: "Important Notice"
* Body: "Hello Michael, welcome to our platform!"
***
### Example 7: Template with Dynamic URL Button
**Body**: Static text
**Buttons**:
* URL button with variable: `"url": "https://example.com/{"{1}"}"` (dynamic)
* Quick reply button: "Contact Support" (static)
**Components returned by API**: `["buttons_url_0"]`
```json theme={null}
{
"contact": {
"number": "34666033135",
"customer_id": "12345"
},
"components_push": {
"buttons_url_0": "https://example.com/profile/{{customer_id}}"
},
"language_template": "en",
"name_template": "account_notification",
"recipient_id": "34666033135",
"notification_type": "utility",
"campaign_name": "Account Updates"
}
```
**Result**: The URL button will link to `https://example.com/profile/12345`
**Note**: The variable in `buttons_url_0` can reference contact fields using `{"{variable}"}` syntax, and the system will resolve it before sending.
***
## 📚 Related Documentation
* **[Authentication](./authentication)** - How to obtain access tokens and API keys
* **[Voice Notifications](./voice_notifications)** - Send automated voice calls
* **[Template Management](./template_management)** - Create and manage WhatsApp templates
* **[Scheduling](./scheduling)** - Advanced scheduling and timezone management
* **[Error Handling](./error_handling)** - Complete error codes and handling strategies
* **[Best Practices](./best_practices)** - Security and performance guidelines
# Authentication
Source: https://docs.lovi.ai/en/api-reference/authentication
Official documentation for authenticating with our API
## Introduction
This section provides the official documentation for authenticating with our API via Postman. It includes detailed instructions on the authentication process, supported methods, and best practices to ensure secure access to the API.
## 🔑 Authentication
To access any Lovi API endpoint, you must first authenticate and obtain an access token using your email and password. This token must then be included in the `Authorization` header of all subsequent requests.
**Token Rotation**: We recommend periodically rotating tokens and storing them securely.
> **Method**: POST **Format**: JSON
### Endpoint
```
POST https://cloud.lovi.ai/functions/v1/auth?grant_type=password
```
### Query Parameters
| Parameter | Required | Fixed Value | Description |
| ------------ | -------- | ----------- | --------------------------------------------------------------------------------------------------------------- |
| `grant_type` | Yes | `password` | Indicates that you're using authentication via email and password. This is currently the only supported method. |
### Request Headers
Inform the server of the type of content you are sending:
| Key | Value | Description |
| -------------- | ------------------ | -------------------------------------------------- |
| `Content-Type` | `application/json` | Indicates that the request body is in JSON format. |
### Request Body
This is where you provide your personal credentials (your email and password):
```json theme={null}
{
"email": "your-email@example.com",
"password": "your-secure-password"
}
```
You should replace:
* "[your-email@example.com](mailto:your-email@example.com)" with your actual email address.
* "your-secure-password" with your password.
### Response
The server will respond with a token that you will need to authenticate in future API calls. The response will look like this:
```json theme={null}
{
"access_token": "your-access-token",
"token_type": "bearer",
"expires_in": 3600,
"expires_at": 1744196340,
"refresh_token": "bNYf6m7S7dFjfHCFTob2zg",
"user": {
"id": "3b601fa8-49fd-4a8c-af8c-b30601410975",
"aud": "authenticated",
"role": "authenticated",
"email": "your-email@example.com",
"email_confirmed_at": "2025-01-16T18:45:09.995114Z",
"phone": "",
"confirmed_at": "2025-01-16T18:45:09.995114Z",
"last_sign_in_at": "2025-04-09T09:59:00.18987884Z",
"app_metadata": {
"provider": "email",
"providers": [
"email"
]
},
"user_metadata": {
"email_verified": true,
"name": "Your-Company"
},
"identities": [
{
"identity_id": "23b66c68-ce37-4483-8470-67e08e21a61e",
"id": "3b601fa8-49fd-4a8c-af8c-b30601410975",
"user_id": "3b601fa8-49fd-4a8c-af8c-b30601410975",
"identity_data": {
"email": "your-email@example.com",
"email_verified": false,
"phone_verified": false,
"sub": "3b601fa8-49fd-4a8c-af8c-b30601410975"
},
"provider": "email",
"last_sign_in_at": "2025-01-16T18:45:09.985377Z",
"created_at": "2025-01-16T18:45:09.985932Z",
"updated_at": "2025-01-16T18:45:09.985932Z",
"email": "your-email@example.com"
}
],
"created_at": "2025-01-16T18:45:09.956743Z",
"updated_at": "2025-04-09T09:59:00.192243Z",
"is_anonymous": false
}
}
```
⚠️ Save your `access_token` — you'll use it to interact with the rest of the API.
***
## 🧭 Retrieve Company API Keys
> **Method**: POST **Format**: JSON
### Endpoint
```
POST https://cloud.lovi.ai/functions/v1/api_keys
```
**Headers**
| **Parameter** | **Example** | **Required** | **Description** |
| ------------- | ------------------------ | ------------ | --------------------------------------------------------------------------------------------------------- |
| Authorization | Bearer your-access-token | Yes | This header is used for authenticating the request. It contains a Bearer token received after logging in. |
### Response
Each object within the **`validApiKeys`** array represents an API key associated with a specific company. The fields included are:
* **`company_id`**: Unique identifier of the company that owns the API key.
* **`key`**: The API key itself, used to authenticate requests.
* **`is_active`**: Boolean value indicating whether the key is currently active (*true*) or inactive (*false*).
* **`last_used`**: Timestamp of the last time the key was used. If the key has never been used, the value will be *null*.
* **`company_name`**: Name of the company associated with the API key, useful for display in interfaces or logs.
Here's an example:
```json theme={null}
{
"validApiKeys": [
{
"company_id": "your_company_id",
"key": "your_key",
"is_active": true,
"last_used": null,
"company_name": "your_company_name"
}
]
}
```
***
### 🚫 API Access Denied (403 Forbidden)
If your company has disabled API access, the server will return a `403 Forbidden` status. This occurs when `apiAccess` is configured to `false` for one or more companies associated with your account.
Example:
```json theme={null}
{
"success": false,
"message": "API access denied for some companies",
"deniedCompanies": [
{
"company_id": "your_company_id",
"company_name": "your_company_name",
"access": false
}
]
}
```
In this case, you must ensure that API access is enabled for your company in the configuration before you can retrieve its keys.
***
### ⚠️ No API Keys Found
If the response returns an empty `validApiKeys` array, it means that no API keys have been created yet for your company.
Example:
```json theme={null}
{
"validApiKeys": []
}
```
In this case, you **must go to the Admin Dashboard** and create a new API key for your company.
In this section, you will see how to configure the profile and also how to configure the API Key from the panel.
> 🧭 **Important**: Without a valid and active API key, you will not be able to authenticate at the company level or perform secured operations.
Once the API key is created and active, it will appear in the `validApiKeys` array and can be used to generate a company-level token.
***
#### 🔁 Token per Company
Once you identify which company you want to work with, you must authenticate again to generate a company-level token. This is usually required for secured actions like sending notifications or uploading media.
You will use the previously retrieved `key` in your query parameters or headers, depending on the endpoint.
# Best Practices & Guidelines
Source: https://docs.lovi.ai/en/api-reference/best_practices
Security, performance, and integration best practices for the Lovi API
## Introduction
This guide covers security best practices, performance optimization, and integration guidelines to help you build robust applications with the Lovi API.
***
## 🔒 Security Best Practices
### API Key Management
**✅ DO:**
* Store API keys in environment variables or secure configuration management
* Use different API keys for different environments (development, staging, production)
* Rotate API keys regularly (recommended: every 90 days)
* Monitor API key usage and set up alerts for unusual activity
* Use the principle of least privilege (separate keys for different functions if available)
**❌ DON'T:**
* Hardcode API keys in source code or commit them to version control
* Share API keys via email, chat, or other unsecured channels
* Use production API keys in development environments
* Log complete API keys in application logs
### Authentication Security
```javascript theme={null}
// ✅ Good - Store in environment variables
const ACCESS_KEY = process.env.LOVI_ACCESS_KEY;
// ❌ Bad - Hardcoded in source
const ACCESS_KEY = 'your-actual-api-key-here';
```
### Request Security
**Always use HTTPS:**
```javascript theme={null}
// ✅ Good - HTTPS only
const BASE_URL = 'https://cloud.lovi.ai';
// ❌ Bad - Insecure HTTP
const BASE_URL = 'http://cloud.lovi.ai';
```
**Validate input data:**
```javascript theme={null}
function validatePhoneNumber(number) {
// Remove any non-digit characters
const cleaned = number.replace(/\D/g, '');
// Check if it's a valid international format
if (!/^\d{10,15}$/.test(cleaned)) {
throw new Error('Invalid phone number format');
}
return cleaned;
}
```
***
## ⚡ Performance Optimization
### Connection Management
**Reuse HTTP connections:**
```javascript theme={null}
// ✅ Good - Reuse connection pool
const https = require('https');
const agent = new https.Agent({
keepAlive: true,
maxSockets: 10
});
const fetch = require('node-fetch');
async function makeRequest(url, data) {
return fetch(url, {
method: 'POST',
agent: agent,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
}
```
### Batch Processing
**Process notifications in batches:**
```javascript theme={null}
async function sendNotificationsBatch(notifications, batchSize = 10) {
const results = [];
for (let i = 0; i < notifications.length; i += batchSize) {
const batch = notifications.slice(i, i + batchSize);
const batchPromises = batch.map(notification =>
sendNotification(notification)
);
const batchResults = await Promise.allSettled(batchPromises);
results.push(...batchResults);
// Rate limiting: wait between batches
if (i + batchSize < notifications.length) {
await sleep(100); // 100ms delay
}
}
return results;
}
```
### Caching Strategies
**Cache template information:**
```javascript theme={null}
class TemplateCache {
constructor(ttl = 3600000) { // 1 hour TTL
this.cache = new Map();
this.ttl = ttl;
}
async getTemplate(templateName) {
const cached = this.cache.get(templateName);
if (cached && Date.now() - cached.timestamp < this.ttl) {
return cached.data;
}
const template = await fetchTemplateFromAPI(templateName);
this.cache.set(templateName, {
data: template,
timestamp: Date.now()
});
return template;
}
}
```
***
## 📊 Rate Limiting & Throttling
### Understanding Rate Limits
| Operation | Limit | Window |
| ------------------ | ------------ | -------- |
| Authentication | 10 requests | 1 minute |
| Notifications | 100 requests | 1 minute |
| Template retrieval | 50 requests | 1 minute |
| Template creation | 5 requests | 1 hour |
### Rate Limit Handling
```javascript theme={null}
class RateLimitHandler {
constructor() {
this.requestQueue = [];
this.processing = false;
}
async makeRequest(url, data) {
return new Promise((resolve, reject) => {
this.requestQueue.push({ url, data, resolve, reject });
this.processQueue();
});
}
async processQueue() {
if (this.processing || this.requestQueue.length === 0) return;
this.processing = true;
while (this.requestQueue.length > 0) {
const request = this.requestQueue.shift();
try {
const response = await this.executeRequest(request);
request.resolve(response);
} catch (error) {
if (error.status === 429) {
// Rate limited - put request back and wait
this.requestQueue.unshift(request);
await sleep(error.retryAfter * 1000);
continue;
}
request.reject(error);
}
// Small delay between requests
await sleep(100);
}
this.processing = false;
}
}
```
***
## 🏗️ Architecture Patterns
### Repository Pattern
```javascript theme={null}
class NotificationRepository {
constructor(apiClient) {
this.apiClient = apiClient;
}
async sendWhatsAppNotification(notification) {
const payload = this.formatWhatsAppPayload(notification);
return await this.apiClient.post('/notify', payload);
}
async sendVoiceNotification(notification) {
const payload = this.formatVoicePayload(notification);
return await this.apiClient.post('/notify/voice', payload);
}
formatWhatsAppPayload(notification) {
return {
contact: {
number: this.validatePhoneNumber(notification.phoneNumber),
name: notification.contactName
},
language_template: notification.language,
name_template: notification.templateName,
recipient_id: notification.phoneNumber,
notification_type: notification.type,
campaign_name: notification.campaign
};
}
}
```
### Factory Pattern for Notifications
```javascript theme={null}
class NotificationFactory {
static create(type, data) {
switch (type) {
case 'whatsapp':
return new WhatsAppNotification(data);
case 'voice':
return new VoiceNotification(data);
default:
throw new Error(`Unknown notification type: ${type}`);
}
}
}
class WhatsAppNotification {
constructor(data) {
this.contact = { number: data.phoneNumber, name: data.name };
this.template = data.templateName;
this.type = 'whatsapp';
}
toPayload() {
return {
contact: this.contact,
name_template: this.template,
// ... other properties
};
}
}
```
***
## 🧪 Testing Strategies
### Unit Testing
```javascript theme={null}
// Mock API responses for testing
const mockAPIClient = {
post: jest.fn()
};
describe('NotificationService', () => {
test('should send WhatsApp notification successfully', async () => {
mockAPIClient.post.mockResolvedValue({
success: true,
notification_id: 'test-123'
});
const service = new NotificationService(mockAPIClient);
const result = await service.sendWhatsApp({
phoneNumber: '34666033135',
templateName: 'welcome',
contactName: 'John Doe'
});
expect(result.success).toBe(true);
expect(mockAPIClient.post).toHaveBeenCalledWith('/notify', expect.any(Object));
});
});
```
### Integration Testing
```javascript theme={null}
// Use test environment with real API calls
describe('Integration Tests', () => {
const testApiKey = process.env.TEST_API_KEY;
const apiClient = new LoviAPIClient(testApiKey);
test('should handle authentication flow', async () => {
const authResult = await apiClient.authenticate(
process.env.TEST_EMAIL,
process.env.TEST_PASSWORD
);
expect(authResult.access_token).toBeDefined();
expect(authResult.token_type).toBe('bearer');
});
});
```
***
## 🔍 Monitoring & Logging
### Structured Logging
```javascript theme={null}
const logger = require('winston');
logger.configure({
format: logger.format.combine(
logger.format.timestamp(),
logger.format.errors({ stack: true }),
logger.format.json()
),
transports: [
new logger.transports.File({ filename: 'api-calls.log' })
]
});
async function loggedAPICall(endpoint, data) {
const requestId = generateRequestId();
logger.info('API request started', {
request_id: requestId,
endpoint: endpoint,
method: 'POST',
timestamp: new Date().toISOString()
});
try {
const response = await makeAPICall(endpoint, data);
logger.info('API request completed', {
request_id: requestId,
endpoint: endpoint,
status_code: response.status,
success: true,
duration: Date.now() - startTime
});
return response;
} catch (error) {
logger.error('API request failed', {
request_id: requestId,
endpoint: endpoint,
error: error.message,
status_code: error.statusCode,
duration: Date.now() - startTime
});
throw error;
}
}
```
### Health Checks
```javascript theme={null}
class APIHealthMonitor {
constructor(apiClient) {
this.apiClient = apiClient;
this.healthStatus = { healthy: true, lastCheck: null };
}
async checkHealth() {
try {
const start = Date.now();
await this.apiClient.get('/templates?limit=1');
const duration = Date.now() - start;
this.healthStatus = {
healthy: true,
lastCheck: new Date().toISOString(),
responseTime: duration,
status: 'operational'
};
} catch (error) {
this.healthStatus = {
healthy: false,
lastCheck: new Date().toISOString(),
error: error.message,
status: 'degraded'
};
}
return this.healthStatus;
}
}
```
***
## 📱 Mobile & Frontend Integration
### React Hook Example
```javascript theme={null}
import { useState, useEffect } from 'react';
function useLoviNotification() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const sendNotification = async (notificationData) => {
setLoading(true);
setError(null);
try {
const response = await fetch('/api/send-notification', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(notificationData)
});
if (!response.ok) {
throw new Error('Failed to send notification');
}
const result = await response.json();
return result;
} catch (err) {
setError(err.message);
throw err;
} finally {
setLoading(false);
}
};
return { sendNotification, loading, error };
}
```
***
## 🔧 Configuration Management
### Environment Configuration
```javascript theme={null}
// config/lovi.js
module.exports = {
development: {
baseURL: 'https://cloud.lovi.ai',
accessKey: process.env.LOVI_DEV_ACCESS_KEY,
rateLimits: {
requests: 10,
window: 60000 // 1 minute
},
retries: 3,
timeout: 30000
},
production: {
baseURL: 'https://cloud.lovi.ai',
accessKey: process.env.LOVI_PROD_ACCESS_KEY,
rateLimits: {
requests: 100,
window: 60000
},
retries: 5,
timeout: 60000
}
};
```
### Client Configuration
```javascript theme={null}
class LoviAPIClient {
constructor(config) {
this.baseURL = config.baseURL;
this.accessKey = config.accessKey;
this.timeout = config.timeout;
this.rateLimiter = new RateLimiter(config.rateLimits);
this.retryCount = config.retries;
}
async request(endpoint, options = {}) {
await this.rateLimiter.acquire();
const url = `${this.baseURL}${endpoint}?access_key=${this.accessKey}`;
const config = {
timeout: this.timeout,
...options
};
return this.retryRequest(url, config);
}
}
```
***
## 📚 Documentation & Team Guidelines
### Code Documentation
```javascript theme={null}
/**
* Send a WhatsApp notification using a template
*
* @param {Object} notification - Notification data
* @param {string} notification.phoneNumber - Recipient phone number (international format, no +)
* @param {string} notification.templateName - Name of approved template
* @param {string} notification.language - Language code (e.g., 'es_ES', 'en_US')
* @param {Object} notification.variables - Template variables
* @param {string} [notification.scheduledTime] - ISO 8601 datetime for scheduling
* @param {string} [notification.timezone] - IANA timezone code
*
* @returns {Promise