API Reporting
=============

Récupérez vos données d'appels et vos statistiques dans vos propres outils : tableaux de bord BI, CRM, rapports planifiés.

Tous les endpoints de cette page sont en lecture seule. Ils relèvent du scope `read` — le même qui liste vos comptes et vos agents. Il n'existe pas de scope dédié au reporting : les scopes d'une clé API servent à contrôler ce qui peut *coûter de l'argent*, ce qu'un rapport ne fait pas.

Statistiques
------------

Les chiffres affichés par la page Statistiques du tableau de bord, sous forme de données. Ils proviennent du même code : les deux ne peuvent pas diverger.

```
GET /v1/statistics
```

**Auth:** API key (`read`)

ParameterTypeDescription  `period`string`today`, `7d` (default), `30d`, `month`, `year` `from`, `to`date`YYYY-MM-DD`. Both required together; overrides `period` `account_id`stringLimit to one account `agent_id`stringLimit to one agent `campaign_id`stringLimit to one campaign `direction`string`INBOUND` or `OUTBOUND` 

```
{
"data": {
"totals": { "calls": 412, "completed": 380, "completion_rate": 92, "avg_duration": 96, "active_now": 3 },
"deltas": { "calls": 12, "avg_duration": -4, "completion_rate": 1 },
"volume": [{ "bucket": "2026-09-08", "label": "8 Sep", "calls": 57 }],
"duration_histogram": [{ "label": "0-15s", "calls": 31 }],
"status_breakdown": { "COMPLETED": 380, "FAILED": 12 },
"direction_split": { "inbound": 300, "outbound": 112 },
"top_agents": [{ "agent_id": "agt_01hx...", "name": "Reception", "calls": 210, "avg_duration": 88, "completion_rate": 94 }],
"quality_trend": [{ "bucket": "2026-09-08", "score": 82, "calls": 41 }],
"sentiment_distribution": { "positive": 240, "neutral": 130, "negative": 42 }
},
"meta": { "from": "...", "to": "...", "timezone": "Europe/Paris", "bucket": "day" }
}
```

### À propos des fuseaux horaires

Les intervalles sont découpés dans **le fuseau horaire de votre organisation**, pas en UTC — « les appels du mardi » désigne le mardi chez vous, exactement comme dans le tableau de bord. Le bloc `meta` indique le fuseau utilisé et la largeur de chaque intervalle.

Lister les appels
-----------------

Tous les appels de votre organisation, du plus récent au plus ancien, tous comptes confondus. Filtrez, puis paginez avec un curseur.

```
GET /v1/calls
```

**Auth:** API key (`read`)

ParameterTypeDescription  `from`, `to`datetimeISO 8601. At most 92 days apart `account_id`, `agent_id`, `campaign_id`stringNarrow to one of each `direction`string`INBOUND` or `OUTBOUND` `status`stringe.g. `COMPLETED`, `FAILED` `disposition`stringe.g. `NO_ANSWER`, `BUSY` `limit`int1-500, default 100 `after`stringCursor: the `next_cursor` of the previous page 

```
{
"data": [
{
"call_id": "cal_01hx...", "account_id": "acc_01hx...", "agent_id": "agt_01hx...",
"agent_version": 7, "campaign_id": null,
"direction": "INBOUND", "status": "COMPLETED", "disposition": null, "ended_by": "agent",
"from_number": "+33612345678", "to_number": "+33988292988",
"duration": 96, "quality_score": 84, "sentiment": "positive",
"recording_available": true,
"queued_at": "...", "answered_at": "...", "ended_at": "...", "created_at": "..."
}
],
"has_more": true,
"next_cursor": "cal_01hx..."
}
```

### Pagination par curseur

Passez le `next_cursor` d'une réponse en paramètre `after` de la requête suivante, et arrêtez-vous quand `has_more` vaut `false`. Un curseur reste stable pendant que de nouveaux appels arrivent : contrairement à un offset, il ne vous montrera pas deux fois le même appel et n'en sautera pas parce qu'un appel est arrivé entre deux pages.

Un appel en détail
------------------

Ajoute le résumé, le résultat des objectifs, les variables collectées par l'agent, les informations de transfert et les coûts.

```
GET /v1/calls/{call_id}
```

**Auth:** API key (`read`)

```
{
"data": {
"call_id": "cal_01hx...",
"summary": "The caller asked to move their appointment to Thursday.",
"objectives": [{ "name": "appointment_booked", "result": "achieved", "value": true }],
"variables": { "customer_email": "marie@example.com" },
"transfer": { "destination": "********6789", "periods": [] },
"costs": { "call": 0.0142, "transfer": null, "termination": null, "llm": 0.0031, "total": 0.0173, "currency": "EUR" },
"links": { "transcript": "/api/v1/calls/cal_01hx.../transcript", "recording": "...", "events": "..." }
}
}
```

`costs.termination` est le coût opérateur du tronçon sortant d'un appel de campagne décroché (`null` sur les appels entrants). Il est facturé sur sa propre ligne et inclus dans `costs.total` : le total correspond donc à la facture.

Événements d'appel
------------------

La chronologie d'un appel : initiation, sonnerie, décroché, fin, ainsi que les étapes d'intégration exécutées par le workflow. Les entrées de diagnostic (latence des modèles, détails de la reconnaissance vocale) ne font pas partie de cet endpoint.

```
GET /v1/calls/{call_id}/events
```

**Auth:** API key (`read`)

```
{
"data": [
{ "seq": 1, "ts_ms": 0, "type": "call_initiated", "service": "call", "transcript_seq": null, "payload": {} },
{ "seq": 8, "ts_ms": 96120, "type": "call_ended", "service": "call", "transcript_seq": 14,
"payload": { "duration_ms": 96120, "endedBy": "agent" } }
]
}
```

`payload` ne contient que ces champs, quand l'événement les possède : `duration_ms`, `endedBy`, `endReason`, `node_id`, `node_name`, `status` et `name`. Un événement qui n'en a aucun revient avec un `payload` vide.

Résultats KPI (objectifs)
-------------------------

Les verdicts sur lesquels votre agent est évalué, appel par appel. Chaque objectif revient avec la valeur collectée, s'il a été atteint, et le raisonnement derrière ce verdict.

```
GET /v1/calls/{call_id}/objectives
```

**Auth:** API key (`read`)

```
{
"data": {
"call_id": "cal_01hx...",
"status": "evaluated",
"quality_score": 84,
"total": 2, "reached": 1, "evaluated": 2,
"items": [
{
"node_id": "n1", "node_label": "Booking",
"variable_name": "appointment_booked", "label": "Appointment booked",
"type": "boolean", "value": "true", "collected": true,
"result": "achieved",
"rationale": "The caller confirmed Thursday 10am."
}
]
}
}
```

The same block is on `GET /v1/calls/{call_id}` under `objectives`. This endpoint is the light one: it does not compute costs, so it is the one to poll.

List rows carry `objectives_total` and `objectives_reached` only — not the rows themselves, so a page of 500 calls stays a page of 500 calls.

### Les objectifs arrivent après l'appel

Ils sont calculés par un job lancé une fois la transcription enregistrée : un appel récupéré dès le déclenchement de son webhook ne les aura pas encore. `status` vous indique dans quel cas vous êtes — et c'est le champ à interroger :

- `pending` — configurés, pas encore calculés. Revenez.
- `evaluated` — un verdict existe. Arrêtez d'interroger.
- `none` — cet agent n'a pas d'objectifs, ou l'appel n'a jamais abouti. Rien n'arrivera.

### unknown n'est pas un échec

`result` vaut `achieved`, `not_achieved` ou `unknown`. `unknown` signifie que l'évaluation n'a pas pu trancher : il ne compte donc ni comme atteint ni comme manqué. `reached` ne compte que les `achieved`, et `evaluated` compte `achieved` plus `not_achieved`. Divisez par `evaluated`, pas par `total`, sinon un objectif indécidable fait chuter votre taux comme s'il avait échoué.

Les valeurs de `result` restent ces identifiants anglais dans toutes les langues. Ils servent à votre code ; `label` et `rationale` sont les parties lisibles par un humain.

Ce sur quoi un agent est évalué
-------------------------------

Les définitions des objectifs, pour construire un rapport avant même qu'un appel ne soit évalué — et pour distinguer une colonne absente d'une colonne vide. Les valeurs autorisées d'un objectif `enum` ne sont disponibles qu'ici.

```
GET /v1/accounts/{account_id}/agents/{agent_id}/objectives
```

**Auth:** API key (`read`)

```
{
"data": {
"agent_id": "agt_01hx...",
"objectives": [
{ "variable_name": "appointment_booked", "label": "Appointment booked",
"description": null, "type": "boolean", "enum_values": [],
"kind": "llm_judge", "node_id": null, "node_label": null },
{ "variable_name": "call_reason", "label": "Call reason",
"description": null, "type": "enum", "enum_values": ["sales", "support"],
"kind": "llm_judge", "node_id": "n1", "node_label": "Booking" }
]
}
}
```

Chaque ligne correspond à une ligne de résultat de `GET /v1/calls/{call_id}/objectives` par `variable_name` et `node_id`. `node_id` et `node_label` indiquent à quelle étape du workflow l'objectif est mesuré (tous deux `null` s'il couvre tout l'appel) ; la même variable définie à deux étapes donne deux lignes. `kind` indique le mode de mesure : `llm_judge` (un LLM lit la transcription), `tool_called` ou `variable_captured` (réponse exacte tirée des événements enregistrés de l'appel, sans jetons) ou `sentiment`. Pour un objectif défini sur une étape du workflow, `variable_name` est le `variableName` de l'étape.

KPI sur un ensemble d'appels
----------------------------

`GET /v1/statistics` renvoie un bloc `kpis` à côté des autres métriques, avec les mêmes filtres.

```
{
"data": {
"kpis": {
"calls": 412, "has_enough_calls": true,
"avg_quality": 81.4, "quality_n": 380, "quality_undersampled": false,
"objectives_total": 824, "objectives_evaluated": 790,
"objectives_reached": 611, "objectives_rate": 77.3,
"connect_rate": 92.1, "no_answer": 18, "busy": 4, "voicemail": 9
}
}
}
```

### Vérifiez la taille de l'échantillon avant de citer un taux

Chaque moyenne est accompagnée du nombre d'appels qui la sous-tend. `has_enough_calls` est faux en dessous de 10 appels et `quality_undersampled` est vrai en dessous de 5 appels notés — un taux d'objectifs de 63 % sur trois appels n'est pas un résultat. Une valeur absente vaut `null`, jamais `0` : `null` signifie qu'il n'y a pas de réponse honnête, alors que `0` voudrait dire que la mesure a été faite et qu'elle est nulle.

Limites
-------

- Une requête couvre au maximum **92 jours**. Utilisez le curseur pour parcourir un historique plus long.
- `limit` vaut 100 par défaut, avec un plafond à 500.
- La limitation de débit est par organisation, comme sur tous les endpoints `/v1`.
- Les tests Agent Lab ne sont jamais inclus — ce ne sont pas des appels que vous avez passés.

Champs
------

La réponse contient exactement les champs listés ici. Nous pourrons en ajouter à l'avenir ; nous n'en supprimerons ni n'en détournerons aucun sans une nouvelle version de l'API. Écrivez donc votre intégration de façon à ignorer les champs qu'elle ne connaît pas.