# Authentication
Source: https://docs.altur.io/en/api-reference/authentication
Authenticate requests to the Altur API using a valid API key for your organization.
## Base URL
The Altur API is built on REST principles and requires HTTPS for all requests, ensuring data security, integrity, and privacy. HTTP requests are not supported.
### API Base URL
```plaintext theme={null}
https://api.altur.io/api/v1.0
```
## Authentication
All requests to the Altur API must include a valid API key to authenticate your organization. Follow the steps below to generate and use your API key.
### Steps to Generate an API Key
Log in to your Altur account using your credentials.
Navigate to the [API Keys](https://app.altur.io/dashboard/api) section in your dashboard.
Click "Generate API Key" to create a new key. Copy it and save it securely, as it will not be displayed again.
## Using Your API Key
Include the following `Authorization` header in your requests to authenticate:
```plaintext theme={null}
Authorization: api-key YOUR_API_SECRET_KEY
```
### Example Request
Here's an example of an authenticated `GET` request:
```curl theme={null}
curl -X GET "https://api.altur.io/api/v1.0/example-endpoint" \
-H "Authorization: api-key YOUR_API_SECRET_KEY" \
-H "Content-Type: application/json"
```
## Best Practices for API Keys
1. **Keep Your Key Secure**
Store your API key in a secure environment, such as environment variables or a secrets manager. Avoid hardcoding keys directly into your application.
2. **Rotate Keys Regularly**
Periodically regenerate your API key to minimize the risk of misuse. Update all systems that use the key to the new value.
3. **Restrict Access**
Assign keys only to trusted systems and personnel. If you suspect your key has been compromised, revoke it immediately and generate a new one.
# Retrieve Call
Source: https://docs.altur.io/en/api-reference/calls/retrieve
GET /call/{id}
Returns the Call corresponding to the given identifier
# Retrieve Call Recording
Source: https://docs.altur.io/en/api-reference/calls/retrieve-recording
GET /call/{id}/recording
Access the recording of a Call in form of a file stream
# Start Call
Source: https://docs.altur.io/en/api-reference/calls/start
POST /call
Starts a Call
# Create Campaign
Source: https://docs.altur.io/en/api-reference/campaigns/create
POST /campaigns
Crea una nueva Campaña para tu organización. Usa `integration_type` para seleccionar entre llamada telefónica o WhatsApp; los campos exclusivos del canal contrario son rechazados.
**Rate Limit:** 12 requests per second
Pass an `Idempotency-Key` header (UUID recommended) so retries don't create
duplicate campaigns.
## Channel-specific Fields
| Field | Channel |
| :-------------- | :----------- |
| `first_message` | `phone_call` |
| `template` | `whatsapp` |
| `template_alt` | `whatsapp` |
Sending a channel-only field for the wrong `integration_type` returns a `400 INVALID_REQUEST`.
## Cycles
`cycles` is optional. When `cycles.enabled` is `false` (or omitted), the rest of the cycles block is ignored. When `cycles.mode` is `cooldown`, `schedule*` fields are rejected. Cycles can be enabled only at creation time; `cycles.enabled` and `cycles.mode` are immutable thereafter.
## Examples
### Phone-call campaign with cooldown cycles
```bash cURL theme={null}
curl -X POST "https://api.altur.io/api/v1.0/campaigns" \
-H "Authorization: api-key YOUR_API_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6f0b2c8c-2a7e-4f5e-9b6a-3a2e8b9c4d10" \
-d '{
"name": "Q2 Reactivation",
"agent_id": "6cy951e6-9877-4d7c-88d7-56b4948d3cce",
"integration_id": "iph_8YqL3mZxR1tV0nKfH9bA",
"integration_type": "phone_call",
"timezone": "America/Mexico_City",
"first_message": "Hi, am I speaking with {name}?",
"cycles": {
"enabled": true,
"mode": "cooldown",
"max_iterations": 3,
"cooldown_minutes": 30,
"filter_statuses": ["converted"]
}
}'
```
```python Python theme={null}
import requests
import uuid
payload = {
"name": "Q2 Reactivation",
"agent_id": "6cy951e6-9877-4d7c-88d7-56b4948d3cce",
"integration_id": "iph_8YqL3mZxR1tV0nKfH9bA",
"integration_type": "phone_call",
"timezone": "America/Mexico_City",
"first_message": "Hi, am I speaking with {name}?",
"cycles": {
"enabled": True,
"mode": "cooldown",
"max_iterations": 3,
"cooldown_minutes": 30,
"filter_statuses": ["converted"],
},
}
response = requests.post(
"https://api.altur.io/api/v1.0/campaigns",
headers={
"Authorization": "api-key YOUR_API_SECRET_KEY",
"Idempotency-Key": str(uuid.uuid4()),
},
json=payload,
)
response.raise_for_status()
campaign = response.json()["campaign"]
print(campaign["id"])
```
```javascript JavaScript theme={null}
const payload = {
name: "Q2 Reactivation",
agent_id: "6cy951e6-9877-4d7c-88d7-56b4948d3cce",
integration_id: "iph_8YqL3mZxR1tV0nKfH9bA",
integration_type: "phone_call",
timezone: "America/Mexico_City",
first_message: "Hi, am I speaking with {name}?",
cycles: {
enabled: true,
mode: "cooldown",
max_iterations: 3,
cooldown_minutes: 30,
filter_statuses: ["converted"],
},
};
const res = await fetch("https://api.altur.io/api/v1.0/campaigns", {
method: "POST",
headers: {
Authorization: "api-key YOUR_API_SECRET_KEY",
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify(payload),
});
const { campaign } = await res.json();
```
### WhatsApp campaign
```bash cURL theme={null}
curl -X POST "https://api.altur.io/api/v1.0/campaigns" \
-H "Authorization: api-key YOUR_API_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Promo June",
"agent_id": "6cy951e6-9877-4d7c-88d7-56b4948d3cce",
"integration_id": "iwa_8YqL3mZxR1tV0nKfH9bA",
"integration_type": "whatsapp",
"template": "promo_june_v1"
}'
```
```python Python theme={null}
import requests
response = requests.post(
"https://api.altur.io/api/v1.0/campaigns",
headers={"Authorization": "api-key YOUR_API_SECRET_KEY"},
json={
"name": "Promo June",
"agent_id": "6cy951e6-9877-4d7c-88d7-56b4948d3cce",
"integration_id": "iwa_8YqL3mZxR1tV0nKfH9bA",
"integration_type": "whatsapp",
"template": "promo_june_v1",
},
)
response.raise_for_status()
```
```javascript JavaScript theme={null}
await fetch("https://api.altur.io/api/v1.0/campaigns", {
method: "POST",
headers: {
Authorization: "api-key YOUR_API_SECRET_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "Promo June",
agent_id: "6cy951e6-9877-4d7c-88d7-56b4948d3cce",
integration_id: "iwa_8YqL3mZxR1tV0nKfH9bA",
integration_type: "whatsapp",
template: "promo_june_v1",
}),
});
```
# Add Contacts to Campaign
Source: https://docs.altur.io/en/api-reference/campaigns/create-contacts
POST /campaigns/{id}/contacts
Crea Contactos de Campaña por lotes. Hasta 1000 contactos por solicitud. Cada item se procesa de forma independiente (una fila inválida no rompe el lote) y se devuelve un `summary` agregado y `results` por item. Usa `if_duplicate` para controlar el comportamiento cuando un contacto ya existe en la campaña (`skip` mantiene el original, `update` sobreescribe los campos mutables).
**Rate Limit:** 12 requests per second
Up to **1000 contacts** per request. Each item is processed independently, so one bad row does not fail the batch. The response includes an aggregate `summary` plus per-item `results`.
Pass an `Idempotency-Key` header (UUID recommended) so retries don't enqueue
duplicate batches.
## Duplicate Handling
`if_duplicate` controls behavior when the contact's phone number already exists in the campaign:
* `skip` (default): keep the existing contact, return `skipped_duplicate`.
* `update`: overwrite `name`, `context`, `extracted_data`, and `f_id` on the existing contact, return `updated`.
## Per-item Result Statuses
| Status | Meaning |
| :------------------ | :---------------------------------------------------------------------- |
| `created` | New contact created |
| `updated` | Existing contact updated (only when `if_duplicate=update`) |
| `skipped_duplicate` | Existing contact preserved |
| `failed` | See `error` (`INVALID_PHONE_NUMBER`, `MISSING_CONTACT`, `SERVER_ERROR`) |
## Examples
```bash cURL theme={null}
curl -X POST "https://api.altur.io/api/v1.0/campaigns/1234/contacts" \
-H "Authorization: api-key YOUR_API_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: c1a45e88-ad34-4f57-9d2e-91a1d4f50f7e" \
-d '{
"if_duplicate": "update",
"contacts": [
{
"f_id": "user_12345",
"name": "John Doe",
"contact": "+521234567890",
"context": "Past-due 30 days, owes $1200",
"extracted_data": { "balance": "1200" }
},
{
"f_id": "user_12346",
"name": "Sofía Pérez",
"contact": "+529876543210"
}
]
}'
```
```python Python theme={null}
import requests
import uuid
batch = {
"if_duplicate": "update",
"contacts": [
{
"f_id": "user_12345",
"name": "John Doe",
"contact": "+521234567890",
"context": "Past-due 30 days, owes $1200",
"extracted_data": {"balance": "1200"},
},
{
"f_id": "user_12346",
"name": "Sofía Pérez",
"contact": "+529876543210",
},
],
}
response = requests.post(
"https://api.altur.io/api/v1.0/campaigns/1234/contacts",
headers={
"Authorization": "api-key YOUR_API_SECRET_KEY",
"Idempotency-Key": str(uuid.uuid4()),
},
json=batch,
)
response.raise_for_status()
body = response.json()
print(body["summary"]) # {"created": 2, "updated": 0, ...}
for item in body["results"]:
if item["status"] == "failed":
print(item["index"], item["error"], item["contact"])
```
```javascript JavaScript theme={null}
const batch = {
if_duplicate: "update",
contacts: [
{
f_id: "user_12345",
name: "John Doe",
contact: "+521234567890",
context: "Past-due 30 days, owes $1200",
extracted_data: { balance: "1200" },
},
{ f_id: "user_12346", name: "Sofía Pérez", contact: "+529876543210" },
],
};
const res = await fetch(
"https://api.altur.io/api/v1.0/campaigns/1234/contacts",
{
method: "POST",
headers: {
Authorization: "api-key YOUR_API_SECRET_KEY",
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify(batch),
},
);
const body = await res.json();
console.log(body.summary);
```
### Example Response
```json theme={null}
{
"success": true,
"summary": { "created": 2, "updated": 0, "skipped_duplicate": 0, "failed": 0 },
"results": [
{ "index": 0, "status": "created", "id": "42", "contact": "+521234567890" },
{ "index": 1, "status": "created", "id": "43", "contact": "+529876543210" }
]
}
```
# Delete Campaign Contact
Source: https://docs.altur.io/en/api-reference/campaigns/delete-contact
DELETE /campaigns/{id}/contacts/{contact_id}
Elimina un contacto de una Campaña. Solo se pueden eliminar contactos en estado `queue` o `failed`; cualquier otro estado devuelve `CONTACT_NOT_DELETABLE` (409).
**Rate Limit:** 12 requests per second
Only contacts in `queue` or `failed` status can be deleted. Anything further
along the lifecycle (`sending`, `sent`, `answered`, etc.) returns
`409 CONTACT_NOT_DELETABLE`.
## Examples
```bash cURL theme={null}
curl -X DELETE "https://api.altur.io/api/v1.0/campaigns/1234/contacts/42" \
-H "Authorization: api-key YOUR_API_SECRET_KEY"
```
```python Python theme={null}
import requests
response = requests.delete(
"https://api.altur.io/api/v1.0/campaigns/1234/contacts/42",
headers={"Authorization": "api-key YOUR_API_SECRET_KEY"},
)
response.raise_for_status() # 204 No Content
```
```javascript JavaScript theme={null}
const res = await fetch(
"https://api.altur.io/api/v1.0/campaigns/1234/contacts/42",
{
method: "DELETE",
headers: { Authorization: "api-key YOUR_API_SECRET_KEY" },
},
);
if (!res.ok) throw new Error(`Delete failed: ${res.status}`);
```
# Lifecycle Actions
Source: https://docs.altur.io/en/api-reference/campaigns/lifecycle
POST /campaigns/{id}/{action}
Aplica una acción de ciclo de vida a una Campaña. Acciones válidas: `activate`, `pause`, `archive`, `resume`. La acción es idempotente: aplicar una acción cuando la campaña ya está en el estado destino es un no-op exitoso.
**Rate Limit:** 12 requests per second
`action` is one of `activate`, `pause`, `archive`, `resume`.
## Valid Transitions
| Action | Allowed source statuses | Effect |
| :--------- | :----------------------------- | :------------------------- |
| `activate` | `pending`, `ready`, `cooldown` | sets `status` → `active` |
| `pause` | `active`, `cooldown` | sets `status` → `inactive` |
| `resume` | `inactive` | sets `status` → `active` |
| `archive` | any | sets `archived = true` |
## Idempotency
* `archive` is always a success. Re-archiving a campaign is a no-op.
* `pause` on a campaign already `inactive`, and `resume` on a campaign already `active`, are no-op successes.
* `activate` is **not** idempotent: calling it on a campaign already in `active` (or `inactive`, or `finished`) returns `409 INVALID_STATUS_TRANSITION` because `active` is not in the allowed source statuses. Use `resume` to re-activate a paused campaign.
## Errors
* `400 UNKNOWN_ACTION`: `action` is not one of the supported values.
* `402 BILLING_LIMIT_REACHED`: `activate` or `resume` and the plan limit was reached.
* `409 INVALID_STATUS_TRANSITION`: current status is not in the allowed sources. The response includes `current_status` and `valid_targets`.
## Examples
```bash cURL theme={null}
curl -X POST "https://api.altur.io/api/v1.0/campaigns/1234/activate" \
-H "Authorization: api-key YOUR_API_SECRET_KEY"
```
```python Python theme={null}
import requests
response = requests.post(
"https://api.altur.io/api/v1.0/campaigns/1234/activate",
headers={"Authorization": "api-key YOUR_API_SECRET_KEY"},
)
response.raise_for_status()
campaign = response.json()["campaign"]
```
```javascript JavaScript theme={null}
const res = await fetch(
"https://api.altur.io/api/v1.0/campaigns/1234/activate",
{
method: "POST",
headers: { Authorization: "api-key YOUR_API_SECRET_KEY" },
},
);
const { campaign } = await res.json();
```
# List Campaigns
Source: https://docs.altur.io/en/api-reference/campaigns/retrieve
GET /campaigns
Returns all Campaigns for your organization
**Rate Limit:** 12 requests per second
`startDate`/`endDate` are legacy names kept for backward compatibility and are actually equivalent to `createdAfter`/`createdBefore` (they filter on `created_at`, not on when the Campaign started or ended). Use `startedAfter`/`startedBefore` and `endedAfter`/`endedBefore` to filter by those dates instead. When both ends of a pair are provided, the *after* value must be earlier than *before*.
# List Campaign Calls
Source: https://docs.altur.io/en/api-reference/campaigns/retrieve-calls
GET /campaigns/{id}/calls
Returns a paginated list of calls for a specific campaign
**Rate Limit:** 12 requests per second
# Retrieve Campaign Contact
Source: https://docs.altur.io/en/api-reference/campaigns/retrieve-contact
GET /campaigns/{id}/contacts/{contact_id}
Devuelve el detalle de un contacto dentro de una Campaña.
**Rate Limit:** 12 requests per second
## Examples
```bash cURL theme={null}
curl "https://api.altur.io/api/v1.0/campaigns/1234/contacts/42" \
-H "Authorization: api-key YOUR_API_SECRET_KEY"
```
```python Python theme={null}
import requests
response = requests.get(
"https://api.altur.io/api/v1.0/campaigns/1234/contacts/42",
headers={"Authorization": "api-key YOUR_API_SECRET_KEY"},
)
response.raise_for_status()
contact = response.json()["contact"]
```
```javascript JavaScript theme={null}
const res = await fetch(
"https://api.altur.io/api/v1.0/campaigns/1234/contacts/42",
{ headers: { Authorization: "api-key YOUR_API_SECRET_KEY" } },
);
const { contact } = await res.json();
```
# List Campaign Contacts
Source: https://docs.altur.io/en/api-reference/campaigns/retrieve-contacts
GET /campaigns/{id}/contacts
Returns the Campaign Contacts corresponding to the given identifier
**Rate Limit:** 12 requests per second
## Status Values by Campaign Type
* `queue` — Contact is waiting in queue
* `sending` — Call is being initiated
* `failed` — Call failed to connect
* `retrying` — Scheduled for retry
* `converted` — Contact was converted
* `voicemail` — Reached voicemail
* `answered` — Call was answered
* `queue` — Message is queued
* `sending` — Message is being sent
* `sent` — Message was sent
* `delivered` — Message was delivered
* `read` — Message was read
* `accepted` — Contact accepted
* `rejected` — Contact rejected
* `failed` — Message failed
* `retrying` — Scheduled for retry
* `converted` — Contact was converted
# Retrieve Campaign
Source: https://docs.altur.io/en/api-reference/campaigns/retrieve-detail
GET /campaigns/{id}
Devuelve el detalle de una Campaña, incluyendo agente, integración, configuración de ciclos y (cuando esté disponible) snapshot de analíticas.
**Rate Limit:** 12 requests per second
The response includes a nested `agent`, `integration`, `cycles` config and, when available, an `analytics` snapshot. The shape of `analytics` depends on the campaign's `integration.type`:
* `phone_call`: call and contact counts and rates (e.g. `calls`, `contactsAnswered`, `contactsConvertedRate`).
* `whatsapp`: message lifecycle counts and rates (e.g. `sent`, `delivered`, `read`, `convertedRate`).
## Examples
```bash cURL theme={null}
curl "https://api.altur.io/api/v1.0/campaigns/1234" \
-H "Authorization: api-key YOUR_API_SECRET_KEY"
```
```python Python theme={null}
import requests
response = requests.get(
"https://api.altur.io/api/v1.0/campaigns/1234",
headers={"Authorization": "api-key YOUR_API_SECRET_KEY"},
)
response.raise_for_status()
campaign = response.json()["campaign"]
analytics = campaign.get("analytics")
```
```javascript JavaScript theme={null}
const res = await fetch("https://api.altur.io/api/v1.0/campaigns/1234", {
headers: { Authorization: "api-key YOUR_API_SECRET_KEY" },
});
const { campaign } = await res.json();
const analytics = campaign.analytics;
```
# Update Campaign
Source: https://docs.altur.io/en/api-reference/campaigns/update
PATCH /campaigns/{id}
Actualización parcial de una Campaña. Los campos inmutables (`agent_id`, `integration_id`, `integration_type`, `cycles.enabled`, `cycles.mode`) no se pueden modificar y devuelven `IMMUTABLE_FIELD`. Las campañas finalizadas son de solo lectura.
**Rate Limit:** 12 requests per second
## Immutable Fields
The following fields cannot be modified after creation. Including them in a PATCH returns `400 IMMUTABLE_FIELD` and identifies the offending field in the response `field` key:
* `agent_id`
* `integration_id`
* `integration_type`
* `cycles.enabled`
* `cycles.mode`
Finished campaigns are read-only and return `409 CAMPAIGN_FINISHED` on any PATCH.
## Cycle Updates
Cycle fields can only be updated on campaigns that have cycles enabled (`400 CYCLES_NOT_ENABLED` otherwise). `schedule`, `schedule_start_date`, and `schedule_end_date` are only valid when `cycles.mode` is `scheduled` (`400 SCHEDULED_MODE_ONLY` otherwise).
## Examples
### Rename a campaign and tighten retries
```bash cURL theme={null}
curl -X PATCH "https://api.altur.io/api/v1.0/campaigns/1234" \
-H "Authorization: api-key YOUR_API_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Q2 Reactivation - wave 2",
"retries": 2
}'
```
```python Python theme={null}
import requests
response = requests.patch(
"https://api.altur.io/api/v1.0/campaigns/1234",
headers={"Authorization": "api-key YOUR_API_SECRET_KEY"},
json={"name": "Q2 Reactivation - wave 2", "retries": 2},
)
response.raise_for_status()
```
```javascript JavaScript theme={null}
await fetch("https://api.altur.io/api/v1.0/campaigns/1234", {
method: "PATCH",
headers: {
Authorization: "api-key YOUR_API_SECRET_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({ name: "Q2 Reactivation - wave 2", retries: 2 }),
});
```
### Update cycle cooldown
```bash cURL theme={null}
curl -X PATCH "https://api.altur.io/api/v1.0/campaigns/1234" \
-H "Authorization: api-key YOUR_API_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"cycles": { "cooldown_minutes": 60 }
}'
```
```python Python theme={null}
requests.patch(
"https://api.altur.io/api/v1.0/campaigns/1234",
headers={"Authorization": "api-key YOUR_API_SECRET_KEY"},
json={"cycles": {"cooldown_minutes": 60}},
).raise_for_status()
```
```javascript JavaScript theme={null}
await fetch("https://api.altur.io/api/v1.0/campaigns/1234", {
method: "PATCH",
headers: {
Authorization: "api-key YOUR_API_SECRET_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({ cycles: { cooldown_minutes: 60 } }),
});
```
# Create EndUser
Source: https://docs.altur.io/en/api-reference/endusers/create
POST /enduser
Create new EndUser for your organization
# Delete EndUser
Source: https://docs.altur.io/en/api-reference/endusers/delete
DELETE /enduser/{id}
Deletes an EndUser
# Retrieve EndUser
Source: https://docs.altur.io/en/api-reference/endusers/retrieve
GET /enduser/{id}
Returns the EndUser corresponding to the given identifier
# Update EndUser
Source: https://docs.altur.io/en/api-reference/endusers/update
PUT /enduser/{id}
Updates the details of an EndUser
# API Overview
Source: https://docs.altur.io/en/api-reference/introduction
Altur's APIs lets you interact programatically with your Altur organization.
Python and JavaScript SDK packages for our API are not available at this time.
## Welcome to the Altur API
Welcome to the Altur API documentation. This guide provides detailed, up-to-date information about our available endpoints, enabling you to manage your organization's communications effectively.
### Current Scope
As of now, the Altur API is designed primarily for messaging integration. It does not yet provide complete functionality for managing chatbots, campaigns, or organizational settings. To perform these tasks, please use the Altur web app.
Open the Altur App on the web
### Why Use the Altur API?
The Altur API allows you to:
* **Automate Conversations**: Streamline user interactions through seamless messaging integrations.
* **Receive Real-Time Data**: Use webhooks to stay informed about important events, such as message statuses or call completions.
* **Customize Workflows**: Leverage our endpoints to build integrations that match your specific business needs.
For a complete list of endpoints and examples, navigate to the sections below.
# Get Messages
Source: https://docs.altur.io/en/api-reference/messages/retrieve
GET /message/{thread_id}
Get all Messages in a Thread, ordered from oldest to most recently sent
# Send Message
Source: https://docs.altur.io/en/api-reference/messages/send
POST /message
Sends Message to the Thread between a Agent and an EndUser, returns the Message created from the request, the Messages generated in response, and returns information about the Thread between the Agent and EndUser
# Retrieve Thread
Source: https://docs.altur.io/en/api-reference/threads/retrieve
GET /thread/{id}
Returns the Thread corresponding to the given identifier
# campaign.cycle_completed
Source: https://docs.altur.io/en/api-reference/webhooks/campaign-cycle-completed
Fires when a cycle iteration of a Campaign finishes, with the iteration number and the timestamp of the next scheduled iteration.
Fires when a cycle iteration of a Campaign finishes. The payload includes the iteration number that just completed and the timestamp of the next scheduled iteration (or `null` if none is scheduled).
Use this to track cycle-by-cycle progress without polling the campaigns endpoint.
* **Trigger**: A cycle iteration of a Campaign completes (regardless of whether more iterations follow).
* **Applies to**: Phone-call campaigns with cycles enabled.
## Request
* **Method**: `POST`
* **Content-Type**: `application/json`
* **Endpoint**: The URL you configure in your webhook integration.
* **Authentication**: `X-Altur-Signature` header for HMAC verification. See [Webhooks Overview](https://docs.altur.io/api-reference/webhooks/overview#securing-your-webhooks).
## Payload Example
```json theme={null}
{
"event_id": "evt_5tH9aB2nQ8vR3mZ1cP0k",
"event_type": "campaign.cycle_completed",
"occurred_at": "2026-06-08T18:00:00.000000-06:00",
"api_version": "1.0",
"project_id": "prj_8YqL3mZxR1tV0nKfH9bA",
"data": {
"campaign": {
"id": 1234,
"name": "Q2 Reactivation",
"status": "cooldown",
"previous_status": null
},
"cycle_iteration": 2,
"next_cycle_at": "2026-06-09T09:00:00.000000-06:00"
}
}
```
### Envelope Fields
| Field | Type | Description |
| :------------ | :------- | :------------------------------------------------------------------ |
| `event_id` | `string` | Unique identifier for this delivery. Use for idempotent processing. |
| `event_type` | `string` | Always `campaign.cycle_completed`. |
| `occurred_at` | `string` | ISO 8601 timestamp in the project's timezone. |
| `api_version` | `string` | Webhook payload schema version (currently `1.0`). |
| `project_id` | `string` | Public identifier of the project that owns the campaign. |
| `data` | `object` | Event payload (see below). |
### `data` Object
| Field | Type | Description |
| :---------------- | :----------------- | :---------------------------------------------------------------------------------------------------------------- |
| `campaign` | `object` | Compact campaign summary (`id`, `name`, `status`, `previous_status`). `previous_status` is `null` for this event. |
| `cycle_iteration` | `int` \| `null` | The iteration number that just completed. |
| `next_cycle_at` | `string` \| `null` | ISO 8601 timestamp of the next scheduled iteration in the project's timezone, or `null` if no further iterations. |
## Response
Return `200 OK` to confirm receipt. Failures are retried per the [delivery retry policy](https://docs.altur.io/api-reference/webhooks/overview#delivery-retry-policy).
## Receiver Examples
For signature verification, see [`is_valid_signature`](https://docs.altur.io/api-reference/webhooks/overview#securing-your-webhooks) in the Webhooks Overview.
```python Python (Flask) theme={null}
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.post("/webhooks/altur/campaign-cycle")
def on_campaign_cycle_completed():
signature = request.headers.get("X-Altur-Signature")
if not is_valid_signature(SHARED_SECRET, request.get_json(), signature):
return jsonify(error="invalid signature"), 401
event = request.get_json()
data = event["data"]
campaign = data["campaign"]
snapshot_cycle_metrics(
campaign_id=campaign["id"],
iteration=data["cycle_iteration"],
next_at=data["next_cycle_at"],
)
if data["next_cycle_at"] is None and campaign["status"] != "finished":
alert_stalled_campaign(campaign["id"])
return "", 200
```
```javascript JavaScript (Express) theme={null}
import express from "express";
const app = express();
app.use(express.json());
app.post("/webhooks/altur/campaign-cycle", (req, res) => {
const signature = req.headers["x-altur-signature"];
if (!isValidSignature(SHARED_SECRET, req.body, signature)) {
return res.status(401).json({ error: "invalid signature" });
}
const { data } = req.body;
const { campaign, cycle_iteration, next_cycle_at } = data;
snapshotCycleMetrics(campaign.id, cycle_iteration, next_cycle_at);
if (next_cycle_at === null && campaign.status !== "finished") {
alertStalledCampaign(campaign.id);
}
res.status(200).send();
});
```
```php PHP theme={null}
# campaign.status_changed
Source: https://docs.altur.io/en/api-reference/webhooks/campaign-status-changed
Fires whenever a Campaign transitions between lifecycle statuses (e.g., active to cooldown, cooldown to finished).
Fires whenever the `status` of a Campaign changes. The payload includes both the previous and new status, so consumers can react to specific transitions (e.g., notify on `finished`, archive on `inactive`).
When `new_status` is `finished`, the payload also includes the campaign's analytics snapshot, so you don't need an extra API call to summarize a finished campaign.
* **Trigger**: Any campaign status transition (`pending` to `ready`, `active` to `inactive`, `active` to `cooldown`, `cooldown` to `finished`, etc.).
* **Filtering**: When configuring the webhook in Altur, you can restrict deliveries to a list of target statuses via the `filters.status` array.
## Request
* **Method**: `POST`
* **Content-Type**: `application/json`
* **Endpoint**: The URL you configure in your webhook integration.
* **Authentication**: `X-Altur-Signature` header for HMAC verification. See [Webhooks Overview](https://docs.altur.io/api-reference/webhooks/overview#securing-your-webhooks).
## Payload Example
```json theme={null}
{
"event_id": "evt_3kQpZ7tA9bN2cV1hX0sR",
"event_type": "campaign.status_changed",
"occurred_at": "2026-06-08T14:23:15.482000-06:00",
"api_version": "1.0",
"project_id": "prj_8YqL3mZxR1tV0nKfH9bA",
"data": {
"campaign": {
"id": 1234,
"name": "Q2 Reactivation",
"status": "finished",
"previous_status": "cooldown"
},
"analytics": {
"calls": 1820,
"callsAnsweredByHuman": 945,
"callsAnsweredByMachine": 612,
"callsAnsweredByUnknown": 263,
"callsAnsweredByHumanRate": 0.519,
"callsAnsweredByMachineRate": 0.336,
"callsAnsweredByUnknownRate": 0.144,
"contacts": 2000,
"contactsProcessed": 1820,
"contactsProcessedRate": 0.91,
"contactsFailed": 80,
"contactsFailedRate": 0.04,
"contactsVoicemail": 612,
"contactsVoicemailRate": 0.306,
"contactsAnswered": 945,
"contactsAnsweredRate": 0.4725,
"contactsConverted": 312,
"contactsConvertedRate": 0.156
}
}
}
```
### Envelope Fields
| Field | Type | Description |
| :------------ | :------- | :------------------------------------------------------------------ |
| `event_id` | `string` | Unique identifier for this delivery. Use for idempotent processing. |
| `event_type` | `string` | Always `campaign.status_changed`. |
| `occurred_at` | `string` | ISO 8601 timestamp in the project's timezone. |
| `api_version` | `string` | Webhook payload schema version (currently `1.0`). |
| `project_id` | `string` | Public identifier of the project that owns the campaign. |
| `data` | `object` | Event payload (see below). |
### `data.campaign` Object
| Field | Type | Description |
| :---------------- | :------- | :---------------------------------------------------- |
| `id` | `int` | Campaign identifier. |
| `name` | `string` | Campaign name. |
| `status` | `string` | The new status (after the transition). |
| `previous_status` | `string` | The status the campaign was in before the transition. |
### `data.analytics` Object (only when `status` = `finished`)
For phone-call campaigns, the snapshot contains call and contact counts and rates. For WhatsApp campaigns it contains message lifecycle counts and rates. See the [`Retrieve Campaign`](https://docs.altur.io/api-reference/campaigns/retrieve-detail) reference for the full shape. The analytics block here matches the `analytics` field on the campaign detail response.
## Response
Return `200 OK` to confirm receipt. Failures are retried per the [delivery retry policy](https://docs.altur.io/api-reference/webhooks/overview#delivery-retry-policy).
## Status Filtering
When configuring the webhook integration, pass `filters.status` as an array of target statuses (e.g., `["finished"]`). Only transitions whose `new_status` is in that list will be delivered. Omitting the filter delivers every status transition.
## Receiver Examples
For signature verification, see [`is_valid_signature`](https://docs.altur.io/api-reference/webhooks/overview#securing-your-webhooks) in the Webhooks Overview.
```python Python (Flask) theme={null}
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.post("/webhooks/altur/campaign-status")
def on_campaign_status():
signature = request.headers.get("X-Altur-Signature")
if not is_valid_signature(SHARED_SECRET, request.get_json(), signature):
return jsonify(error="invalid signature"), 401
event = request.get_json()
data = event["data"]
campaign = data["campaign"]
if campaign["status"] == "finished":
analytics = data.get("analytics") or {}
archive_campaign_in_crm(campaign["id"], analytics)
elif campaign["status"] == "inactive":
pause_downstream_billing(campaign["id"])
return "", 200
```
```javascript JavaScript (Express) theme={null}
import express from "express";
const app = express();
app.use(express.json());
app.post("/webhooks/altur/campaign-status", (req, res) => {
const signature = req.headers["x-altur-signature"];
if (!isValidSignature(SHARED_SECRET, req.body, signature)) {
return res.status(401).json({ error: "invalid signature" });
}
const { data } = req.body;
const { campaign } = data;
if (campaign.status === "finished") {
archiveCampaignInCrm(campaign.id, data.analytics ?? {});
} else if (campaign.status === "inactive") {
pauseDownstreamBilling(campaign.id);
}
res.status(200).send();
});
```
```php PHP theme={null}
# on_call_end
Source: https://docs.altur.io/en/api-reference/webhooks/on-call-end
Triggered at the end of a call to provide detailed Call and End User information.
## About This Event
The `on_call_end` webhook is triggered at the conclusion of a call in the Altur platform. This webhook provides detailed information about the call, the agent involved, and the campaign user. Use this webhook to keep your backend or CRM updated with the latest call data.
## Event Details
* **Trigger**: Automatically triggered at the end of a call.
* **Purpose**: To update your backend or CRM with call status, transcript, recording URL, and user information.
## Request Details
* **HTTP Method**: `POST`
* **Content Type**: `application/json`
* **Endpoint**: The URL you configure in your webhook integration settings.
* **Authentication**: Includes the `X-Altur-Signature` header for HMAC verification.
## Payload Example
Here’s an example of the payload sent with the `on_call_end` event:
```json theme={null}
{
"event_type": "on_call_end",
"id": "cll_gBoLtd02pUI50QwXkFjB",
"thread_id": "thr_Kp7mQz3XvNb8LcRt2sYw",
"type": "outbound",
"status": "ended",
"phone_number_from": "+000000000000",
"phone_number_to": "+000000000000",
"answered_by": "human",
"created_at": "2024-12-19T15:00:10Z",
"started_at": "2024-12-19T15:00:10Z",
"ended_at": "2024-12-19T15:00:10Z",
"ended_by": "user",
"ended_reason": "user_hangup",
"duration": 45,
"billed_duration": 45,
"recording_url": "https://api.altur.io/api/v1.0/call/cll_gBoLtd02pUI50QwXkFjB/recording",
"transcript": [
{
"sent_by": "assistant",
"sent_at": "2024-12-19T15:00:00Z",
"content": "Hello am I speaking with Sofía?"
},
{
"sent_by": "user",
"sent_at": "2024-12-19T15:00:10Z",
"content": "Yes, who is this?"
}
],
"assistant": {
"id": "6cy951e6-9877-4d7c-88d7-56b4948d3cce",
"nickname": "DebtCollectorBot",
"name": "Diana Arroyo"
},
"end_user": {
"id": "2be951e6-7798-47cd-878d-49b2918a3bba",
"status": "converted",
"display_name": "John Doe",
"phone_number": "+521234567890",
"context": "Customer data...",
"extracted_data": {
"Payment Amount": "1200",
"Payment Date": "2024-12-26",
"Payment Medium": "app"
},
"tags": ["payment_promise", "call_later"]
},
"campaign_user": {
"f_id": "user_12345",
"name": "John Doe",
"contact": "+521234567890",
"context": "Customer data for campaign...",
"status": "converted",
"failed_retries": 0,
"voicemail_retries": 1,
"extracted_data": {
"Payment Amount": "1200",
"Payment Date": "2024-12-26",
"Payment Medium": "app"
},
"tags": ["payment_promise", "call_later"]
}
}
```
### Root Fields
| Field | Type | Description |
| :------------------ | :--------- | :-------------------------------------------------------------------------------------------------- |
| `event_type` | `string` | Event type (`on_call_end`). |
| `id` | `string` | Unique identifier for the call. |
| `thread_id` | `string` | Unique identifier of the Thread the call belongs to. |
| `type` | `string` | Call type (`outbound`or `inbound`). |
| `status` | `string` | Status of the call enum value (e.g., `ended`). |
| `phone_number_from` | `string` | E.164 formatted number. |
| `phone_number_to` | `string` | E.164 formatted number. |
| `answered_by` | `string` | Indicates who answered the call (`human`, `machine` or `unknown`). |
| `created_at` | `datetime` | ISO formatted date time of when the call was created. |
| `started_at` | `datetime` | ISO formatted date time of when the call started. |
| `ended_at` | `datetime` | ISO formatted date time of when the call ended. |
| `ended_by` | `string` | Indicates who ended the call (`agent`, `user` or `system`). Empty if the call never connected. |
| `ended_reason` | `string` | Brief reason why the call ended. Empty if not available. |
| `duration` | `int` | Call duration in seconds. |
| `billed_duration` | `int` | Billed call duration in seconds. |
| `recording_url` | `string` | URL to [download](https://docs.altur.io/api-reference/calls/retrieve-recording) the call recording. |
| `transcript` | `array` | Call transcript object. |
### Assistant Object
| Field | Type | Description |
| :--------- | :------- | :----------------------------------- |
| `id` | `uuid` | Unique identifier for the assistant. |
| `nickname` | `string` | Assistant's nickname. |
| `name` | `string` | Assistant's name. |
### End User Object
**Deprecated**: The `end_user` object is deprecated and will be removed in a
future version. Please use the `campaign_user` object instead for
campaign-related calls.
| Field | Type | Description |
| :--------------- | :------- | :--------------------------------------------------------------------------------------------- |
| `id` | `uuid` | Unique identifier for the end user. |
| `status` | `string` | Current status of the user enum value (e.g., `converted`). |
| `display_name` | `string` | Display name of the user. |
| `phone_number` | `string` | User's phone number (E.164 format). |
| `context` | `string` | Additional user-specific information. |
| `extracted_data` | `object` | Key-value pairs of extracted data. |
| `tags` | `array` | Array of tag names corresponding to tags assigned to the last campaign user or current thread. |
### Campaign User Object
| Field | Type | Description |
| :------------------ | :------- | :--------------------------------------------------------- |
| `f_id` | `string` | Foreign identifier for the campaign user from your system. |
| `name` | `string` | Name of the campaign user. |
| `contact` | `string` | Contact phone number (E.164 format). |
| `context` | `string` | Additional campaign user-specific information. |
| `status` | `string` | Current status of the campaign user (e.g., `converted`). |
| `failed_retries` | `int` | Number of retry attempts on failed calls. |
| `voicemail_retries` | `int` | Number of voicemail retry attempts. |
| `extracted_data` | `object` | Key-value pairs of data extracted during the call. |
| `tags` | `array` | Array of tag names assigned to the campaign user. |
## Response Expectations
Your endpoint should return a 200 OK status code to confirm receipt of the webhook. For information on retry behavior in case of failures, see the [Retry Policy](https://docs.altur.io/api-reference/webhooks/overview#delivery-retry-policy) section in the Webhook Overview.
## Security
Each request includes an HMAC signature in the `X-Altur-Signature` header. Refer to the [Securing Your Webhooks](https://docs.altur.io/api-reference/webhooks/overview#securing-your-webhooks) section for instructions on validating webhook requests.
## Example Use Cases
1. **CRM Integration**
Use the `on_call_end` webhook to update call logs and user interaction data in your CRM system.
2. **Analytics**
Process transcripts and user status to generate insights or trigger follow-up actions.
3. **Data Enrichment**
Automatically store extracted data, like account numbers or due dates, in your system for further use.
# Webhooks Overview
Source: https://docs.altur.io/en/api-reference/webhooks/overview
Receive real-time updates from Altur directly to your backend or CRM.
## Introduction to Webhooks
Webhooks allow you to receive real-time notifications about events in your Altur organization. When a specific event occurs (e.g., the end of a call), Altur sends an HTTP POST request with event-specific data to your configured endpoint. This enables seamless integration with your backend systems or CRM, ensuring they remain up-to-date.
## How Webhooks Work
1. **Configure Your Endpoint**\
You can configure your webhook URL in the Altur platform. This URL will receive POST requests when specific events are triggered.
2. **Receive Notifications**\
When an event occurs, Altur sends a POST request with the event's details in a structured JSON format to your endpoint.
3. **Acknowledge Delivery**\
Your endpoint should respond with a `200 OK` status code to confirm successful receipt. Altur waits up to 5 seconds for a response. If the request fails, times out, or returns a non-2xx status, Altur retries with exponential backoff (up to 5 total attempts).
## Event Types
Webhooks can be triggered by the following event types:
* [`on_call_end`](https://docs.altur.io/api-reference/webhooks/on-call-end): Triggered at the end of a call, this webhook sends detailed information about the call along with basic information about the AI agent and the end user.
* [`campaign.status_changed`](https://docs.altur.io/api-reference/webhooks/campaign-status-changed): Triggered when a Campaign transitions between lifecycle statuses. Includes the analytics snapshot when the campaign reaches `finished`.
* [`campaign.cycle_completed`](https://docs.altur.io/api-reference/webhooks/campaign-cycle-completed): Triggered when a cycle iteration of a Campaign finishes, with the completed iteration number and the next scheduled iteration timestamp.
Campaign webhooks use a versioned envelope (`event_id`, `event_type`,
`occurred_at`, `api_version`, `project_id`, `data`). Use `event_id` for
idempotent processing on your side. The `on_call_end` event predates this
envelope and keeps its flat top-level shape.
## Securing Your Webhooks
To ensure secure communication and verify the authenticity of webhook requests, Altur includes an HMAC signature in the `X-Altur-Signature` header for every request.
### How It Works
* **Shared Secret**: Each webhook integration is configured with a unique base64-encoded shared secret key.
* **HMAC Generation**: Altur generates an HMAC SHA-256 hash using the base64-decoded shared secret and the JSON request payload (serialized without spaces). This hash is base64-encoded and included in the `X-Altur-Signature` header.
* **Validation**: Your endpoint must validate the signature using the same shared secret and JSON serialization format.
### Validation Function Example
```python Python theme={null}
import json
import base64
import hmac
import hashlib
def is_valid_signature(secret: str, payload: Union[str, dict], signature: str) -> bool:
"""
Validate Altur webhook signature.
Args:
secret: Base64-encoded shared secret key
payload: Request payload (dict or JSON string)
signature: Base64-encoded signature from X-Altur-Signature header
"""
# Convert payload to consistent JSON format (compact, no spaces)
if isinstance(payload, dict):
payload_bytes = json.dumps(payload, separators=(',', ':')).encode('utf-8')
else:
payload_bytes = payload.encode('utf-8')
# Decode the base64 secret
secret_bytes = base64.b64decode(secret)
# Generate expected signature
expected_signature = base64.b64encode(
hmac.new(secret_bytes, payload_bytes, hashlib.sha256).digest()
).decode()
return hmac.compare_digest(signature, expected_signature)
```
```javascript JavaScript theme={null}
const crypto = require("crypto");
function isValidSignature(secret, payload, signature) {
/**
* Validate Altur webhook signature.
*
* @param {string} secret - Base64-encoded shared secret key
* @param {object|string} payload - Request payload (object or JSON string)
* @param {string} signature - Base64-encoded signature from X-Altur-Signature header
*/
// Convert payload to consistent JSON format (compact, no spaces)
const payloadString =
typeof payload === "string" ? payload : JSON.stringify(payload);
// Decode base64 secret
const secretBuffer = Buffer.from(secret, "base64");
// Generate expected signature
const expectedSignature = crypto
.createHmac("sha256", secretBuffer)
.update(payloadString)
.digest("base64");
// Compare signatures using timing-safe comparison
return crypto.timingSafeEqual(
Buffer.from(signature, "base64"),
Buffer.from(expectedSignature, "base64")
);
}
// Usage example
const signature = req.headers["x-altur-signature"];
const isValid = isValidSignature(yourSecret, req.body, signature);
```
```php PHP theme={null}
function isValidSignature($secret, $payload, $signature) {
/*
* Validate Altur webhook signature.
*
* @param string $secret Base64-encoded shared secret key
* @param array|string $payload Request payload (array or JSON string)
* @param string $signature Base64-encoded signature from X-Altur-Signature header
*/
// Convert payload to consistent JSON format (compact, no spaces)
$payloadString = is_string($payload)
? $payload
: json_encode($payload, JSON_UNESCAPED_SLASHES);
// Decode base64 secret
$secretBytes = base64_decode($secret);
// Generate expected signature
$expectedSignature = base64_encode(
hash_hmac('sha256', $payloadString, $secretBytes, true)
);
// Compare signatures using timing-safe comparison
return hash_equals($signature, $expectedSignature);
}
// Usage example
$signature = $_SERVER['HTTP_X_ALTUR_SIGNATURE'];
$isValid = isValidSignature($yourSecret, json_decode(file_get_contents('php://input'), true), $signature);
```
### Usage Example
```python Example 1: Using parsed JSON dict (recommended) theme={null}
import json
request_body = request.get_json() # Parsed JSON from your web framework
signature = request.headers.get('X-Altur-Signature')
shared_secret = "your-base64-encoded-shared-secret"
if is_valid_signature(shared_secret, request_body, signature):
print("Valid webhook request!")
else:
print("Invalid webhook request!")
```
```python Example 2: Using raw JSON string theme={null}
payload = '{"type":"on_call_end","status":"ended"}' # Raw request payload (compact JSON)
signature = "provided-signature-from-header" # X-Altur-Signature header
shared_secret = "your-base64-encoded-shared-secret" # Base64-encoded shared secret
if is_valid_signature(shared_secret, payload, signature):
print("Valid webhook request!")
else:
print("Invalid webhook request!")
```
### Headers Sent with Webhook
* `Content-Type`: `application/json`
* `X-Altur-Signature`: Base64-encoded HMAC SHA-256 hash of the compact JSON payload
### Why This Matters
This mechanism ensures:
* The request originates from Altur.
* The payload has not been tampered with during transmission.
## Delivery Retry Policy
If your endpoint does not respond with a 2xx status code (or does not respond within the 5-second request timeout), Altur retries the delivery:
* **Maximum attempts**: 5 (initial delivery plus 4 retries).
* **Backoff**: 60s after the first failure, doubling each attempt (60s, 2m, 4m, 8m).
If all attempts fail, the event is marked as failed and will not be delivered again.
## Example Workflow
Here’s a typical workflow for handling webhooks effectively:
1. **Set Up Your Endpoint**
Create an API endpoint in your backend, such as `/webhooks/altur/on-call-end`. Ensure the endpoint is publicly accessible and configured to accept POST requests with a `Content-Type` of `application/json`.
2. **Parse Incoming Data**
Extract and process the JSON payload sent by Altur. Ensure your code handles possible issues, such as malformed payloads or missing fields, to prevent runtime errors.
3. **Validate Request Authenticity**
Use the `X-Altur-Signature` header to verify the authenticity of the request. This involves:
* Recomputing the HMAC signature using the shared secret and payload.
* Comparing it with the signature provided in the header.
4. **Respond Promptly**
Return a `200 OK` status code to acknowledge successful receipt. Altur times out the request after 5 seconds, so respond well before that to avoid unnecessary retries.
5. **Log Events for Debugging**
Log the incoming request and processing steps to ensure traceability. This is particularly useful for debugging issues with payloads or retries.
6. **Handle Retries Gracefully**
Design your system to handle retries in case of temporary failures. Ensure your endpoint is idempotent so processing the same event multiple times does not create duplicate actions (e.g., duplicate database entries or API calls).
## Best Practices
1. **Secure Your Endpoint**
* **Authenticate Requests**: Verify webhook authenticity using HMAC signatures. Always use a secure and unique shared secret for each integration.
* **Restrict Access**: Use IP whitelisting or firewalls to allow requests only from Altur's servers.
* **Encrypt Communication**: Ensure your webhook endpoint uses HTTPS to encrypt data in transit.
2. **Log Events**
* **Record Everything**: Log all incoming webhook requests, including timestamps, headers, payloads, and responses.
* **Monitor Failures**: Implement alerts for repeated failures or invalid requests to quickly address issues.
* **Enable Debugging**: Retain logs for a reasonable period to aid in debugging or auditing as necessary.
3. **Test Thoroughly**
* **Simulate Events**: Use the Altur dashboard to simulate webhook events and confirm your endpoint handles them correctly.
* **Handle Edge Cases**: Test scenarios like malformed payloads, large payloads, or missing fields to ensure robustness.
* **Use Staging Environments**: Set up a dedicated staging endpoint for safe testing without affecting production systems.
4. **Optimize Performance**
* **Respond Quickly**: Altur times out after 5 seconds. If processing takes longer, acknowledge with `200 OK` immediately and handle the work asynchronously.
* **Minimize Processing**: Perform lightweight validation and queuing at the webhook level, offloading heavy processing to background workers.
* **Use Caching**: Cache static responses for redundant requests when appropriate.
5. **Ensure Idempotency**
* **Avoid Duplicate Actions**: Design your system to handle retries without causing duplicate operations (e.g., database inserts or API calls). Use unique event identifiers from Altur for this purpose.
6. **Communicate Failures**
* **Return Meaningful Status Codes**: Respond with appropriate HTTP status codes to help diagnose issues (e.g., `400` for invalid requests or `500` for server errors).
* **Log and Notify**: Log failures and consider notifying your team if critical retries are exhausted.
## Troubleshooting Signature Validation
If you're having trouble with signature validation, check these common issues:
### 1. JSON Serialization Format
Ensure you're using the same JSON serialization format as Altur:
* **Compact format**: No spaces after colons or commas
* **Consistent ordering**: Use the exact payload received
```python theme={null}
# ✅ Correct - compact JSON
json.dumps(data, separators=(',', ':'))
# ❌ Wrong - includes spaces
json.dumps(data) # Results in: {"key": "value"}
```
### 2. Secret Key Format
* Ensure your secret key is **base64-encoded**
* The secret should be the same one configured in your Altur webhook integration
### 3. Header Name
* The signature header is `X-Altur-Signature` (case-sensitive in some frameworks)
* Make sure you're reading the correct header
### 4. Timing Attacks
Always use timing-safe comparison functions:
* Python: `hmac.compare_digest()`
* Node.js: `crypto.timingSafeEqual()`
* PHP: `hash_equals()`
### 5. Testing Your Implementation
You can test your signature validation with this example:
```python theme={null}
# Test data
test_payload = {"test": "data"}
test_secret = "dGVzdC1zZWNyZXQ=" # base64 for "test-secret"
expected_signature = "ZjM2ZTc4YWJkZDQ1ZGZlYjM4NTIwYWY1ZmY1MzFkMTk4YmM0YzJmMzU0MTJjMmE3MGZjZGY4ZDhkOTYzOWY4OA=="
# This should return True
result = is_valid_signature(test_secret, test_payload, expected_signature)
print(f"Validation result: {result}")
```
# Follow-ups
Source: https://docs.altur.io/en/documentation/agents/follow-ups
Make your agent automatically place follow-up calls or send messages
**Follow-ups** are a tool that Altur offers so your agent can schedule and execute WhatsApp calls or messages according to the conversation it has with each contact in your campaign.
This feature is configurable within each **agent** and you can **customize it** according to the execution parameters you define.
***
## Before you start
* **Permissions:** make sure you have permissions to edit the agent/project.
* **Integrations:** if you will use WhatsApp and/or calls, confirm that your lines/templates are **approved** and connected.
* **Consent:** use outbound WhatsApp only with contacts who have given **prior consent** to avoid the risk of being blocked by Meta.
### Accessing Follow-up Settings
1. **Access your agent's configuration**: Go to the **Agents** section in the left sidebar.
* If your agent is already created, access its configuration by clicking on it.
* If your agent is not yet created, click **New agent** to start the configuration.
2. **Go to the Follow-up tab**: Once inside your agent, you will see the configuration sections on the left side. Locate **Follow-ups** and access it by clicking.
***
## 🔧 Configuration
### 1. Allowed schedule for follow-up
Limit the hours during which the agent can **execute** follow-up calls or messages.
* **Example:** `08:00 – 20:00`
### 2. Allowed days for follow-up
Define which days your agent can perform follow-ups.
* **Typical example:** **Mon–Sat** active, **Sun** inactive.
### 3. Enable follow-up - Calls
Enable the agent to schedule follow-ups via **calls** using the toggle.
#### **When to schedule calls:**
Indicate to the agent the criteria it will use to decide **when** to schedule a follow-up call, based on the conversation context.
> Make sure to indicate in which scenario it should schedule a follow-up as well as the instructions so it can define the day and time when the call will be placed.
#### **Instructions for the call:**
Script that the agent will follow **during** the follow-up call. Works the same as the call instructions in the **Behavior** section of your agent.
#### Maximum number of retries:
Define how many times to **retry** contacting if the call is not answered.
* **Example:** `3`.
### 4. Enable follow-up - WhatsApp
**Use the toggle** to enable the agent to schedule a follow-up via **WhatsApp**.
#### **When to schedule messages**
Indicate to the agent the criteria it will use to decide (according to the conversation with the contact) **when** to schedule sending a follow-up message via WhatsApp.
> Make sure to indicate in which scenario it should schedule a follow-up, as well as the instructions so it can define the day and time when it will send the message.
#### **Instructions for conversation (WhatsApp)**
Guide for **tone and structure** that the agent will follow to interact in the follow-up conversation via WhatsApp. Works the same as the WhatsApp instructions in the **Behavior** section of your agent.
#### WhatsApp Templates
In this section, select the template that the agent will use to start the conversation.
### 5. Save changes
Before saving, review:
1. Correct schedule and days
2. Retries configured
3. "When to schedule" and "Instructions" texts complete and without ambiguities.
Once you have reviewed it, save the changes by clicking the button you will find in the lower right corner.
***
## 🚀 Launch
Once follow-up via call and/or WhatsApp is active, your agent will begin to automatically schedule the sending of calls or messages within your campaigns.
***
## ✅ Viewing scheduled follow-ups
You can see scheduled follow-ups in two ways:
1. **Within your campaign:** in the **Follow-ups** section
2. **In your Inbox:** within conversations, in the texts that appear below the call recording.
***
# Campaign Creation
Source: https://docs.altur.io/en/documentation/campaigns/create
Learn how to create efficient sales and collection campaigns with Altur.
Campaigns are a core feature of Altur. They allow you to automatically launch large-scale conversations without sacrificing personalization in each interaction.
## Types of Campaigns
Altur currently supports two channels for campaigns: **Phone Calls** and **WhatsApp**.
We recommend using **WhatsApp campaigns** only when your contact list has previously given consent to receive messages from your company. This is due to strict regulations around high-volume outbound messaging, and non-compliance may result in account sanctions or blocks.
## Configuring a Campaign
Follow these steps to create a new campaign in Altur:
1. **Go to the Campaigns section:** Navigate to the **Campaigns** section on the left sidebar and click **New Campaign** in the top-right corner.
If you don't see these options, ask your organization’s administrator to grant you the necessary permissions.
2. **Fill in the basic fields:**
* **Campaign name:** Choose a short, descriptive name.
* **Description:** Add internal details or the goal of the campaign.
3. **Select an agent:** Choose the pre-configured **agent** that will handle the conversations for this campaign.
4. **Select an integration:** Pick the phone number or WhatsApp line from which messages or calls will be sent.
If you're using WhatsApp, make sure the account and number are properly configured and integrated with Altur.
5. **Set the time zone:** Choose the time zone of your contact base. By default, it is set to **America/Mexico\_City**.
6. **Initial message (Phone Calls):** Enter the initial message that the agent will say when the call connects.
7. **Template (WhatsApp):** Select one or two pre-approved WhatsApp message templates.\
If the selected template contains an image header, an image upload field will be enabled. Make sure the image matches Meta's approved content.
8. **Configure retries:** Set how many times the system will attempt to contact a customer in case of no response or voicemail.
9. **(Optional) Configure retry loops (Phone Calls only):** You can enable automatic loops so the campaign retries contacting unconverted leads. When enabled, you can configure:
* Max number of loops per day
* Cooldown time between loops (in minutes)
* Status filters (e.g. skip users who already answered)
* Tag filters (e.g. skip numbers marked as invalid)
10. **Set daily send limits (WhatsApp):** Define how many messages can be sent per day. This helps avoid saturating the integration or exceeding WhatsApp limits.
11. **Save the campaign:** Click **Save** to create the new campaign with your configuration.
Done! Your campaign has been created.
Once saved, the campaign will remain in **“pending”** state until you upload your contact list.
## Uploading Contacts
To run a campaign, you need to upload a list of contacts. Altur supports `.csv` files encoded in UTF-8 or similar formats.
### 📄 CSV File Structure
Your file must include the following **required** columns:
| Column | Description |
| :-------- | :----------------------------------------------------------------------------------------------------------------- |
| `contact` | Phone number in any format. Altur will automatically convert it to international E.164 format. |
| `name` | The contact's name, used to personalize the conversation. |
| `context` | Additional information the agent can use in the conversation (e.g. offer details, payment amount, due date, etc.). |
You may also include these **optional** columns:
| Column | Description |
| :------------------------ | :---------------------------------------------------------------------------------------------------------------------- |
| `id` | A unique identifier from your system. Altur will store it for internal reference. |
| `override_agent_name` | A custom name for the agent when speaking to this contact (overrides the default agent name). |
| `override_agent_voice_id` | The voice ID to be used specifically for this contact. Only available for phone campaigns. Must match a valid voice ID. |
Invalid phone numbers or duplicate contacts will be skipped automatically.
### 🧪 Example of a valid file
```contacts.csv theme={null}
contact,name,context,id,override_agent_name,override_agent_voice_id
+528715551234,Juan Pérez,"Amount: $1,500. Due: June 5",123,Inés,female_voice
+525555555678,Ana Gómez,"Amount: $900. Due: June 7",124,Andres,male_voice
+523385559012,Carlos Ruiz,"Amount: $2,300. Due: June 6",125,Inés,female_voice
```
### ✅ Best Practices
* Use a comma (`,`) as a delimiter, which is the default in most CSV editors (Excel, Google Sheets, etc.).
* Avoid special characters in the column headers.
* Make sure there are no leading or trailing spaces in the values.
* Validate your contact data before uploading it.
## Launching the Campaign
Once the campaign is fully configured and the contact list has been uploaded:
### ✅ Final Review
* Review the **campaign summary** to confirm the configuration, time zone, and number of contacts.
* Ensure everything looks good before proceeding.
### 🚀 Launch
* Click **Start Campaign** to begin sending calls or messages according to the configured schedule.
### 📊 Real-Time Monitoring
From the campaign dashboard you can:
* View the total number of interactions.
* Track failed attempts, voicemails, retries, and converted contacts.
* Monitor the overall progress of the campaign.
### 📁 Exporting Results
* During or after the campaign, you can **export a CSV file** containing:
* Contact handling results
* Details of calls or messages sent
* Extracted data from interactions
This helps you extract results, analyze campaign performance, or create follow-up lists.
### 🔁 Repeat or Archive
* If the campaign uses **loops**, they will run automatically according to your settings.
* If not, you can:
* **Repeat the campaign** by uploading a filtered CSV (e.g. only non-converted contacts).
* **Archive the campaign** if no further actions are needed.
# Campaign Results Interpretation
Source: https://docs.altur.io/en/documentation/campaigns/results
A brief guide on how to correctly interpret campaign results in Altur.
After running a campaign, Altur allows you to download multiple `.csv` files. This guide details the content of each one and how to correctly interpret the results.
### Contact Export
This CSV file contains **all contacts** added to the campaign along with their current status and associated metrics.
#### File Columns
**Basic Columns:**
* `campaign_name`: Campaign name.
* `campaign_date`: Campaign creation date (format: YYYY-MM-DD).
* `id`: Unique contact identifier from the import file (if provided).
* `contact`: Phone number in E.164 format.
* `name`: Contact name.
* `context`: Context or additional contact information.
* `status`: Current contact status in the campaign (see status section).
* `retries`: Number of retries performed.
* `calls`: Total number of calls made to the contact (call campaigns only).
* `billed_duration`: Total billable duration in readable format (call campaigns only).
* `last_update`: Date and time of last status update.
* `follow_up`: Indicates if the contact has a scheduled follow-up (`True`/`False`).
**Dynamic Columns:**
* **Tags**: One column per tag configured in the agent. The value will be `True` if the contact has that tag, `False` otherwise.
* **Extractable Fields**: One column per data field configured for extraction in the agent (e.g., "email", "interest", "rating").
#### Campaign Contact Statuses
Contacts can have different statuses depending on the campaign channel.
##### Common Statuses (Both Channels)
| Status | Description |
| ----------- | ------------------------------------------- |
| `queue` | Queued, waiting to be processed |
| `sending` | Sending message or initiating call |
| `failed` | General failure in sending/call |
| `retrying` | Retrying after failed status or no response |
| `converted` | Contact marked as converted by the system |
##### Call-Specific Statuses
| Status | Description |
| ----------- | -------------------------- |
| `voicemail` | Call answered by voicemail |
| `answered` | Call answered |
##### WhatsApp-Specific Statuses
| Status | Description |
| ----------- | --------------------------------- |
| `sent` | Message sent |
| `delivered` | Message delivered |
| `read` | Message read |
| `accepted` | Contact accepted the conversation |
| `rejected` | Contact rejected the conversation |
**Deprecated Statuses (Legacy Data Only):** The following statuses are no longer assigned to new records but may appear when querying legacy data created before December 2025:
* `no-answer` → Now maps to `failed`
* `busy` → Now maps to `failed`
* `unreachable` → Now maps to `failed`
**Conditional:** These statuses can change to `queue` or `retrying` if the
system determines there are available retries according to the campaign
configuration.
#### Understanding Retries
* `retries`: Retry counter specific to calls that reach voicemail. The limit is configured in the campaign.
* `failed_retries` (not visible in CSV): Internal system that retries contacts with `failed` status. Limit defined in system configuration (typically 1).
**Retry Logic:**
1. If a contact reaches `voicemail` and `retries < campaign_limit`, the status will change to `retrying` and the contact will be attempted again.
2. If a contact has `failed`, and has not exceeded the system retry limit, it will return to `queue`.
3. When limits are exceeded, the status remains as final.
### Call Export
This CSV file contains **each individual call** made during the campaign, allowing detailed analysis of each interaction.
#### File Columns
**Basic Columns:**
* `campaign_name`: Campaign name.
* `campaign_date`: Campaign creation date.
* `id`: Unique call ID.
* `number_to`: Destination phone number (E.164).
* `contact_name`: Contact name.
* `type`: Call type (`inbound` / `outbound`).
* `status`: Final call status (see status section).
* `answered_by`: Who answered the call (see next section).
* `created_at`: Call creation date and time.
* `started_at`: Call start date and time (when answered).
* `ended_at`: Call end date and time.
* `ended_by`: Who ended the call (`agent`, `user` or `system`). Empty if the call never connected.
* `ended_reason`: Brief reason why the call ended. Empty if not available.
* `duration`: Total duration in readable format (MM:SS).
* `billed_duration`: Billable duration in readable format (MM:SS).
* `recording_url`: URL to download the call recording.
**Dynamic Columns:**
* **Tags**: One column per tag configured in the agent.
* **Extractable Fields**: Data extracted during the conversation.
#### Call Statuses
Individual calls can have the following statuses (`status`):
| Status | Description |
| ------------- | -------------------------- |
| `created` | Call created in the system |
| `queued` | Call queued to be dialed |
| `ringing` | Phone ringing |
| `in-progress` | Call in progress |
| `forwarding` | Call being transferred |
| `ended` | Call ended successfully |
| `busy` | Line busy |
| `no-answer` | No answer |
| `failed` | Call error |
#### Response Types (answered\_by)
This field is critical to understand who answered the call:
| Value | Description | Billing Impact |
| --------- | ----------------------------- | -------------------------- |
| `human` | Answered by a human | Full rate |
| `machine` | Answered by answering machine | Rate limited to 5 seconds |
| `unknown` | Could not be determined | According to configuration |
**Important note on billing:**
* Calls answered by answering machines (`machine`) have a reduced rate.
* The billable duration (`billed_duration`) automatically applies the limit to voicemail calls.
* Only calls with `answered_by: human` are included in the recording export.
#### Difference between Duration and Billed Duration
* `duration`: Total time from when the call was answered until it ended.
* `billed_duration`: Time billed as consumption:
* For `human`: equal to `duration`.
* For `machine`: `duration` limited to 5 seconds.
### Results Export
This CSV file is a **filtered version** of the contact export that includes **only contacts with extracted data**.
#### Differences from Contact Export
* **Includes**: Only contacts with extracted data.
* **Purpose**: Analysis of conversions and positive campaign results.
* **Columns**: Identical to the contact export.
#### Use Cases
This export is useful for:
* Analyzing qualified leads that completed the conversation flow.
* Obtaining structured information collected during conversations (emails, dates, preferences, etc.).
* Calculating effective conversion rates.
* Integrating with CRMs or other downstream systems.
***
#### Campaign Statuses
| Status | Description |
| --------------------- | ----------------------------------- |
| `Pending (pending)` | Campaign created, awaiting contacts |
| `Ready (ready)` | Contacts loaded, ready to activate |
| `Active (active)` | Campaign running |
| `Inactive (inactive)` | Campaign manually paused |
| `Cooldown (cooldown)` | Between cycles, waiting to resume |
| `Finished (finished)` | All cycles completed |
#### Cycle Interpretation in Results
When a campaign has cycles enabled:
* Contacts that do not reach a "successful final status" will be retried.
* The `retries` field resets on each cycle.
* Statuses within the filter for cycles will not return to queue in the next cycle.
***
### Best Practices for Analysis
1. **To measure reach**: Use **Contact** export and count statuses:
* **For Calls:**
* `answered`
* `converted`
* **For WhatsApp:**
* `delivered`
* `accepted`
* `read`
* `converted`
2. **To measure conversion**: Use **Contact** export and filter/count contacts in `converted` status or use **Results** and analyze extracted fields.
3. **For cost auditing**: Use **Call** export and sum `billed_duration` by `answered_by`.
***
# Introduction
Source: https://docs.altur.io/en/documentation/introduction
Welcome to Altur's documentation. Explore detailed guides, API references, and more to help you maximize Altur's capabilities.
## Welcome to Altur
Welcome to Altur, your all-in-one platform for automating payment collections at scale. Using advanced AI agents, Altur transforms the debt collection process by seamlessly integrating phone calls and WhatsApp conversations. Our mission is to help businesses streamline their operations, increase recovery rates, and reduce reliance on traditional methods.
This documentation is your go-to guide for everything Altur. Whether you're setting up your first AI agents, exploring our advanced features, or troubleshooting, you'll find the resources and guidance you need right here.
## What You Will Find
* **Comprehensive Guides**
Step-by-step tutorials for setting up and using Altur's features.
* **API Reference**
Technical details for integrating Altur into your systems.
* **Best Practices**
Tips and recommendations to optimize your organization's communications.
## Key Features and Components
Altur empowers businesses with cutting-edge technology tailored to the debt collection industry. Here are some of our key features:
Automate debt collection with intelligent agents that handle calls and
WhatsApp messages, escalating to human agents when needed.
Easily create, manage, and monitor debt collection campaigns tailored to
your goals, with real-time analytics to track performance.
Access all customer interactions in one place, allowing agents to track,
respond, and manage conversations seamlessly.
Monitor calls in real-time, analyze outcomes, and extract actionable
insights to improve your processes.
## Why Choose Altur?
Altur stands out as the premier solution for automating and optimizing debt collection processes. Here's why:
1. **Tailored for Debt Collection**
Altur is purpose-built for the debt collection industry. Our platform addresses the unique challenges of this field, ensuring a perfect fit for agencies, contact centers, and financial institutions.
2. **Seamless Omnichannel Experience**
Unlike traditional platforms, Altur enables seamless communication across phone and WhatsApp, allowing for consistent and unified interactions with customers.
3. **Real-Time Collaboration**
Empower your human agents to monitor and intervene in ongoing conversations, ensuring that critical cases are handled with precision and care.
4. **Localized Expertise**
Our deep understanding of markets like Mexico and Latin America gives us an edge in delivering solutions that resonate with regional needs, providing unparalleled results.
5. **Proven Results**
Altur has consistently delivered measurable improvements for clients, increasing recovery rates while simplifying operations.
6. **Scalable and Future-Ready**
As your business grows, Altur scales with you, leveraging the latest in AI and communication technologies to stay ahead of industry trends.
# Sub-processors
Source: https://docs.altur.io/en/documentation/resources/subprocessors
Altur engages the following entities to carry out processing activities for Customer Data.
## Third-party Sub-processors
| Entity | Purpose |
| :------------------ | :---------------------------------------- |
| Cloudflare, Ltd. | WAF, CDN, DNS. |
| Amazon Web Services | Cloud infrastructure. |
| Vercel | Web application deployment and delivery. |
| OpenAI | Natural language generation and analysis. |
| ElevenLabs | TTS model. |
| Deepgram | STT model. |