API Campagnes
=============

Créez des campagnes d'appels sortants, alimentez-les en contacts en continu, lancez-les et mettez-les en pause, lisez leur progression contact par contact, et tenez à jour les listes d'exclusion et d'opposition, tout cela depuis vos propres systèmes. La vue opérateur de la même fonctionnalité se trouve sur la page [Campagnes](https://www.manivox.ai/docs/calls/campaigns).

Authentification et chemin de base
----------------------------------

Les opérations de cette page sont disponibles de deux façons, au comportement identique :

- **Avec une clé API**, sous `/api/v1/campaigns…` — Bearer `la_…`, la clé devant disposer de la permission `campaigns:write` pour créer, modifier ou piloter une campagne (`read` suffit pour lister et consulter). Les écritures acceptent un en-tête `Idempotency-Key` afin qu'une requête réessayée ne démarre pas deux fois la campagne. C'est ce qu'il faut utiliser depuis vos propres systèmes ; voir [Authentification](https://www.manivox.ai/docs/api/index).
- **Avec l'authentification de session**, sous `/api/campaigns…` — cookies + token CSRF du dashboard, rôle **User** ou **Admin** requis. C'est ce que le dashboard lui-même utilise.

Les chemins et payloads ci-dessous sont écrits avec le préfixe `/api` ; pour la version par clé API, ajoutez `/v1`. Deux différences à connaître : la version par clé API de [Mettre à jour une campagne](#update-campaign) n'accepte que `PATCH`, là où la route de session accepte `PUT` et `PATCH` ; et la version par clé API renvoie la campagne dans sa forme stable et documentée (voir [l'objet campagne](#campaign-object)) plutôt que l'enregistrement complet. Dans les deux cas, chaque objet est limité à votre organisation : un id de campagne, de contact ou d'agent d'une autre organisation renvoie `404`.

L'objet campagne
----------------

```
{
"id": "camp_01hx...",
"name": "Spring renewal outreach",
"description": null,
"agent_id": "agt_01hx3k9z...",
"caller_id": "+33988292988",
"prefix": null,
"status": "ARMED",
"paused_reason": null,
"scheduled_at": null,
"started_at": "2026-06-02T08:00:12.000000Z",
"completed_at": null,
"canceled_at": null,
"rate_limit": 5,
"rate_interval": 60,
"max_parallel_calls": 3,
"ringing_timeout_seconds": 35,
"max_attempts_per_contact": 3,
"voicemail_policy": "SKIP",
"timezone": null,
"calling_windows": { "1": [[10, 13], [14, 20]], "2": [[10, 13], [14, 20]], "3": [[10, 13], [14, 20]], "4": [[10, 13], [14, 20]], "5": [[10, 13], [14, 20]] },
"retry_policy": null,
"csv_mapping": { "phone_column": "Phone", "variable_columns": { "First Name": "first_name" } },
"total_contacts": 950,
"processed_count": 210,
"success_count": 131,
"failed_count": 79,
"import_status": "DONE",
"import_report": { "valid": 950, "invalid": 12, "duplicate": 3, "do_not_call": 2, "excluded": 0, "invalid_reasons": { "not_a_number": 12 } },
"created_at": "2026-06-01T15:04:00.000000Z",
"updated_at": "2026-06-02T08:00:12.000000Z"
}
```

C'est l'enregistrement que renvoient les endpoints de session. Avec une clé API, la même campagne revient **sans** `canceled_at`, `processed_count`, `success_count`, `failed_count`, `csv_mapping` et `import_report`, et **avec** deux champs supplémentaires : `agent_name` (le nom du workflow) et `callback_window_days` (le nombre de jours pendant lesquels un contact qui rappelle est encore reconnu comme contact de cette campagne). Les compteurs et le rapport d'import sont disponibles via les endpoints de rapport et de contacts ci-dessous.

### Statuts

StatutSignification   `IDLE`Ne compose pas. Un brouillon quand `started_at` est `null`, en pause sinon ; `paused_reason` vaut `BY_OPERATOR`, `INSUFFICIENT_BALANCE`, `AGENT_UNAVAILABLE`, `NO_CALLER_ID` ou `null`. `ARMED`Activée. Le composeur la reprend chaque minute et compose tout ce qui est dû dans les limites de la cadence, des plages horaires d'appel et de la simultanéité. Une campagne armée dont le `scheduled_at` est dans le futur attend ; en dehors de ses plages horaires, elle saute ; tant que chaque contact attend l'écoulement d'un délai de nouvelle tentative, elle est silencieuse. Aucun de ces cas n'est un statut distinct. `COMPLETED`Chaque contact a atteint un état final. Positionné par le composeur, jamais par cette API. Terminal. `CANCELED`Arrêtée définitivement par un opérateur ou par `POST /cancel`. Terminal. **Mettre en pause ou annuler ne raccroche jamais un appel déjà en cours.** Les deux actions empêchent seulement le composeur de réclamer de nouveaux contacts. Un agent en pleine conversation continue de parler jusqu'à la fin naturelle de l'appel.

Créer une campagne
------------------

```
POST /api/campaigns
```

**Auth :** session du dashboard

```
{
"name": "Spring renewal outreach",
"agent_id": "agt_01hx3k9z...",
"caller_id": "+33988292988",
"rate_limit": 5,
"rate_interval": 60,
"max_parallel_calls": 3,
"ringing_timeout_seconds": 35,
"max_attempts_per_contact": 3,
"voicemail_policy": "SKIP",
"calling_windows": { "1": [[10, 13], [14, 20]], "2": [[10, 13], [14, 20]] },
"retry_policy": { "BUSY": 900, "NO_ANSWER": 10800 },
"scheduled_at": "2026-06-02T08:00:00+02:00",
"contacts": [
{ "phone": "+33612345678", "variables": { "first_name": "Marie" } }
],
"exclude": ["+33698765432"]
}
```

ChampObligatoireTypeDescription   `name`OuistringJusqu'à 255 caractères. `description`NonstringJusqu'à 1000 caractères. `agent_id`OuistringL'agent dont la version live s'exécute sur chaque appel décroché. Ne peut plus être changé une fois la campagne armée. `caller_id`OuistringLe numéro présenté aux personnes que vous appelez, au format E.164. Il doit s'agir de l'un des numéros de votre organisation **rattachés à cet agent** ; tout autre numéro renvoie `422`. C'est ce qui achemine les rappels vers le bon agent. `prefix`NonstringPréfixe de numérotation composé uniquement de chiffres, ajouté devant chaque numéro composé. Omettez-le sauf si votre interlocuteur Manivox.ai vous en a donné un. `rate_limit`, `rate_interval`NonintegerCombien d'appels peuvent être *démarrés* par intervalle de `rate_interval` secondes. Omettez `rate_limit` pour n'appliquer aucune limite de cadence. `max_parallel_calls`NonintegerCombien d'appels de cette campagne peuvent être en cours en même temps. Omis, il suit la cadence d'appel. Le quota d'appels simultanés de votre organisation, moins une part réservée aux appels entrants, reste toujours un plafond également. `ringing_timeout_seconds`NonintegerCombien de temps laisser sonner un téléphone. 35 par défaut. `max_attempts_per_contact`NonintegerTentatives par contact, premier appel compris. 3 par défaut. `voicemail_policy`Nonstring`SKIP` (par défaut : le contact est clos quand un répondeur est détecté) ou `RETRY` (traité comme n'importe quel autre résultat transitoire). `MESSAGE` est encore accepté pour compatibilité mais se comporte actuellement comme `SKIP`. `timezone`NonstringFuseau horaire IANA pour les plages horaires d'appel et `scheduled_at`. Omis (recommandé), le fuseau horaire de l'organisation s'applique, y compris s'il est corrigé plus tard. `calling_windows`NonobjectHeures pendant lesquelles la campagne peut composer ; voir [ci-dessous](#calling-windows). Par défaut, les heures de bureau françaises. Envoyez `null` pour autoriser la numérotation à toute heure. `retry_policy`NonobjectDélai par résultat avant la tentative suivante, en **secondes** ; voir [ci-dessous](#retry-policy). `scheduled_at`NondatetimeHeure de démarrage. Envoyez un décalage horaire (`2026-06-02T08:00:00+02:00`) ; une heure sans décalage est lue dans le fuseau horaire de la campagne. **Le renseigner arme la campagne** : elle est créée `ARMED` et attend. Omettez-le pour créer un brouillon `IDLE` que vous lancez explicitement. `csv_mapping`Nonobject`{"phone_column": ..., "variable_columns": {"Column": "variable_name"}}`. N'a de sens que lorsque les contacts proviennent d'un CSV importé dans le dashboard ; les contacts envoyés en ligne portent leurs propres `variables`. `contacts`NonarrayJusqu'à 1000 contacts à ingérer immédiatement (`{"phone": ..., "variables": {...}}`), normalisés, filtrés et dédoublonnés exactement comme lors d'un [ajout](#append-contacts). Une liste plus grande passe par des ajouts répétés. `exclude`NonarrayJusqu'à 1000 numéros que cette campagne ne doit jamais appeler (sa [liste d'exclusion](#exclusions)). Appliqué avant l'ingestion des contacts envoyés en ligne. **Réponse, 201 Created** : l'objet campagne, avec `status` `IDLE` ou `ARMED` selon `scheduled_at`. Les échecs de validation renvoient `422` avec l'objet `errors` indexé par champ décrit dans [Format d'erreur](https://www.manivox.ai/docs/api/index#errors).

### Plages horaires d'appel

```
{
"1": [[10, 13], [14, 20]],
"2": [[10, 13], [14, 20]],
"3": [[10, 13], [14, 20]],
"4": [[10, 13], [14, 20]],
"5": [[10, 13], [14, 20]]
}
```

Les clés sont les jours de la semaine ISO (`1` = lundi … `7` = dimanche) ; les valeurs sont des listes de paires d'heures semi-ouvertes `[début, fin)`, de sorte que `[14, 20]` signifie « peut composer jusqu'à 19:59 ». L'objet ci-dessus est la valeur par défaut appliquée quand vous omettez le champ. Les heures sont évaluées dans le fuseau horaire de la campagne (celui de l'organisation sauf si vous en définissez un). Un objet vide est refusé : il signifierait « jamais », ce qu'aucun client n'a jamais voulu dire. En dehors de sa plage horaire, une campagne reste `ARMED` et saute ; elle n'est pas mise en pause.

### Politique de nouvelles tentatives

```
{ "BUSY": 900, "NO_ANSWER": 10800, "UNREACHABLE": 21600, "CONGESTION": 300, "FAILED": 1800 }
```

Délai en secondes avant qu'un contact soit retenté, par résultat. Seules les clés que vous envoyez remplacent les valeurs par défaut indiquées ci-dessus (15 min, 3 h, 6 h, 5 min, 30 min) ; utilisez `null` pour « ne jamais retenter ce résultat ». `INVALID` et `REJECTED` ne sont jamais retentés quelle que soit la configuration, `CARRIER_CAPPED` est retenté peu après sans consommer de tentative, et `VOICEMAIL` suit `voicemail_policy`. `max_attempts_per_contact` est le plafond absolu, tous résultats confondus.

Lister les campagnes
--------------------

```
GET /api/campaigns
```

**Auth :** session du dashboard

Renvoie toutes les campagnes de votre organisation, les plus récentes en premier, chacune avec son `agent` (`id`, `name`) et un `calls_count`.

Obtenir une campagne
--------------------

```
GET /api/campaigns/{id}
```

**Auth :** session du dashboard

Renvoie la campagne plus un résumé `progress` en direct, calculé à partir des lignes de contacts elles-mêmes, jamais à partir de compteurs en cache :

```
{
"id": "camp_01hx...",
"status": "ARMED",
"progress": {
"total": 950,
"processed": 210,
"outstanding": 740,
"by_status": {
"PENDING": 700, "DIALING": 40, "COMPLETED": 180,
"FAILED": 20, "INVALID": 8, "DO_NOT_CALL": 2
}
}
}
```

Mettre à jour une campagne
--------------------------

```
PUT /api/campaigns/{id}       # session : PUT ou PATCH
PATCH /api/v1/campaigns/{id}  # clé API : PATCH seulement, PUT n'est pas accepté
```

**Auth :** session du dashboard, ou clé API avec `campaigns:write`

Accepte n'importe quel sous-ensemble des champs de création, à l'exception de `contacts` et `exclude` (utilisez les endpoints dédiés). `status` n'est pas modifiable ici ; utilisez les actions de cycle de vie ci-dessous. `agent_id` ne peut changer que tant que la campagne n'est pas `ARMED` (`422` sinon), et un nouveau `caller_id` est validé par rapport aux numéros de cet agent. Sur une campagne armée, les changements s'appliquent au prochain cycle de numérotation.

Supprimer une campagne
----------------------

```
DELETE /api/campaigns/{id}
```

**Auth :** session du dashboard

Supprime la campagne, ses lignes de contacts, sa liste d'exclusion et son CSV importé. Refusé avec `409` tant qu'elle est `ARMED` : mettez-la d'abord en pause ou annulez-la. Renvoie `204`.

Lancer, mettre en pause, reprendre, annuler
-------------------------------------------

```
POST /api/campaigns/{id}/start
POST /api/campaigns/{id}/pause
POST /api/campaigns/{id}/resume
POST /api/campaigns/{id}/cancel
```

**Auth :** session du dashboard

- `start` arme une campagne `IDLE`, efface `paused_reason` et respecte un `scheduled_at` futur. Passez `{"start_now": true}` pour effacer la planification et composer immédiatement. Lancer une campagne déjà `ARMED` est idempotent. `422` sur une campagne terminale.
- `pause` remet une campagne `ARMED` en `IDLE` avec `paused_reason` `BY_OPERATOR`. `422` sinon.
- `resume` est un alias de `start`, conservé pour les clients existants.
- `cancel` est terminal : `422` si la campagne l'est déjà.

Chacune renvoie l'objet campagne mis à jour.

Ajouter des contacts
--------------------

```
POST /api/campaigns/{id}/contacts
```

**Auth :** session du dashboard

Des contacts peuvent être ajoutés à une campagne **déjà en train de composer** : les campagnes en alimentation continue sont prises en charge par conception. Les nouvelles lignes deviennent composables immédiatement. Seules les campagnes `COMPLETED` et `CANCELED` refusent de nouveaux contacts (`422`).

Le corps de la requête est un tableau JSON brut, pas un objet, d'au plus **1000** contacts par requête :

```
[
{ "phone": "+33612345678", "variables": { "first_name": "Marie" } },
{ "phone": "0612345679" }
]
```

Chaque numéro est normalisé en E.164 (les formes `+33…`, `33…` et nationale `0…` sont acceptées ; tout ce que la plateforme ne peut pas lire avec certitude est refusé plutôt que deviné), puis vérifié par rapport aux contacts déjà présents dans la campagne, à la liste d'opposition du compte et à la liste d'exclusion de la campagne. `variables` contient les valeurs par contact que votre agent lit sous la forme `{{variable_name}}` ; une clé qui entre en collision avec une variable système est ignorée. La réponse rend compte de ce qui est arrivé au lot :

```
{
"valid": 1,
"invalid": 0,
"duplicate": 0,
"do_not_call": 1,
"excluded": 0,
"invalid_reasons": {}
}
```

**Réponse, 201 Created.** Ce même format de rapport est celui que produit un import CSV dans le dashboard.

Lister les contacts d'une campagne
----------------------------------

```
GET /api/campaigns/{id}/contacts
```

**Auth :** session du dashboard

ParamètreTypeDescription   `status`stringFiltre par statut de contact : `pending`, `dialing`, `completed`, `failed`, `invalid`, `do_not_call`. Les valeurs inconnues renvoient `422`. `disposition`stringFiltre par dernier résultat : `answered`, `busy`, `no_answer`, `unreachable`, `invalid`, `rejected`, `congestion`, `carrier_capped`, `voicemail`, `voicemail_message_left`, `called_back`, `failed`. `per_page`integerTaille de page, 50 par défaut, plafonnée à 200. Renvoie une charge utile de paginateur standard (`data`, `total`, `per_page`, `current_page`…), triée par numéro de ligne d'origine du contact. Chaque contact porte `phone`, `phone_raw`, `variables`, `status`, `attempts`, `last_disposition`, `next_attempt_after`, `last_call_id`, `callback_count` et `last_callback_call_id`.

Liste d'exclusion de la campagne
--------------------------------

```
GET    /api/campaigns/{id}/exclusions
POST   /api/campaigns/{id}/exclusions
DELETE /api/campaigns/{id}/exclusions/{phone}
```

**Auth :** session du dashboard

Les numéros que *cette campagne* ne doit pas appeler. Ce n'est délibérément pas la liste d'opposition : laisser des clients existants hors d'une action d'acquisition n'équivaut pas à la déclaration « ne m'appelez plus jamais ». `GET` renvoie `{"campaign_id", "count", "phones": [...]}`. `POST` prend `{"phones": ["+33698765432", ...]}` (jusqu'à 1000) et renvoie `201` avec `added`, `already_listed`, `invalid`, `invalid_examples` et `contacts_stopped` : les contacts correspondants encore en attente de numérotation sont clos immédiatement, même sur une campagne armée ; un contact dont l'appel est en cours garde sa conversation. `DELETE` avec le numéro en E.164 (encodez le `+` dans l'URL) renvoie `{"status": "deleted"}`, ou `404` avec `{"status": "not_listed"}` ; il ne réactive jamais les contacts arrêtés.

Liste d'opposition
------------------

```
GET    /api/do-not-call
POST   /api/do-not-call
DELETE /api/do-not-call/{id}
```

**Auth :** session du dashboard

La liste d'opposition permanente, par compte. Chaque campagne d'un compte est filtrée avec la liste de ce compte au moment de l'import et à nouveau au moment de la numérotation.

- `GET` liste les entrées, les plus récentes en premier, paginées. Le paramètre facultatif `account_id` restreint à un compte plus les entrées historiques à l'échelle de l'organisation (exactement ce avec quoi les campagnes de ce compte sont filtrées) ; `search` correspond à une partie d'un numéro ; `source` vaut `manual`, `import` ou `opt_out`.
- `POST` prend `{"account_id": "acc_...", "phones": ["+33612345678", "0698765432"], "reason": "CRM sync 2026-06"}` : `account_id` et `phones` (1 à **10 000**) sont obligatoires, `reason` est facultatif (255 caractères). Les numéros sont normalisés, les formes issues de journaux d'appels sans le `+` conviennent donc. Renvoie `201` avec `added`, `already_listed`, `invalid`, `invalid_examples` et `contacts_stopped` : les contacts des campagnes de ce compte encore en attente de numérotation sont clos dans la même requête.
- `DELETE /{id}` supprime une entrée (`{"status": "deleted"}`). Il ne réintègre **pas** les contacts arrêtés dans une campagne ; réajouter quelqu'un passe par un ajout de contact explicite.