> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kameleoon.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Exporter les résultats d'expériences vers Notion

> Récupérez une expérience et ses résultats avec l'Automation API, déterminez la variation la plus performante à partir de la probabilité de succès bayésienne, et upsertez l'enregistrement dans une base de données Notion à l'aide d'un seul script Python.

Récupérez une expérience et ses résultats avec l'Automation API, transformez-les en propriétés de page Notion, et upsertez l'enregistrement dans une base de données Notion à l'aide d'un seul script Python.

## Objectif

Ce tutoriel décrit, étape par étape, le fonctionnement du script [kameleoon\_to\_notion.py](/assets/developer-docs/script/apis/automation-api-rest/tutorials/kameleoon_to_notion.py.zip). À partir d'un **ID d'expérience** Kameleoon et d'un **ID de base de données** Notion, le script récupère les métadonnées de l'expérience et ses résultats statistiques, détermine la variation la plus performante, mappe les données sur une base de données Notion *Experiments*, et écrit l'enregistrement dans Notion. Réexécuter le script pour la même expérience met à jour la page existante au lieu d'en créer un doublon.

Les étapes 1 à 5 réutilisent le même flux de demande et d'interrogation que le [tutoriel d'export vers Airtable](./exporting-experiment-results-to-airtable) : seule la destination change.

<Note>
  L'API de Notion ne dispose pas d'upsert natif. Le script en simule un : il interroge la source de données de la base de données pour trouver une page dont le titre correspond au nom de l'expérience, puis met à jour cette page si elle existe ou en crée une nouvelle sinon. Ce tutoriel cible la version `2025-09-03` de l'API Notion, qui organise chaque base de données autour d'une ou plusieurs **sources de données**.
</Note>

## Prérequis

* **Identifiants API Kameleoon.** L'Automation API nécessite un access token. Le script en obtient un de manière programmatique à partir d'un `client_id` et d'un `client_secret` grâce au grant `client_credentials`. Consultez [Obtenir un access token](/fr/developer-docs/apis/automation-api-rest/get-started/get-started#1-obtenir-un-token-d’accès).

* **Un token d'intégration interne Notion.** Créez une intégration sur [notion.so/my-integrations](https://www.notion.so/my-integrations) et copiez son token.

* **Une base de données Notion** avec un schéma *Experiments* et les propriétés suivantes : `Experiment Name` (title), `Status` (select), `Start date` (date), `End date` (date), `Notes` (rich text), `Actual` (number), `Probability` (select), et `Result` (select).

* **L'ID de base de données Notion.** Ouvrez la base de données en pleine page (l'ID est la chaîne de 32 caractères dans son URL, avant le paramètre de vue `?v=`).

* **Python 3.9 ou supérieur** avec la bibliothèque `requests` (`pip install requests`).

<Warning>
  Partagez la base de données avec votre intégration, sinon chaque requête retourne `object_not_found`. Ouvrez la base de données, allez dans **`•••` → Connections → Add connections**, et sélectionnez votre intégration.
</Warning>

<Warning>
  Stockez tous les identifiants dans des variables d'environnement. N'écrivez jamais les secrets en dur dans le script.
</Warning>

```bash theme={null}
export KAMELEOON_CLIENT_ID="..."
export KAMELEOON_CLIENT_SECRET="..."
export NOTION_TOKEN="..."
```

Le tutoriel utilise l'expérience d'exemple **Product Page Redesign** (ID `188308`), avec deux variations en plus de l'originale : *Redesign 1* (ID `828220`) et *Redesign 2* (ID `828221`).

## 1. S'authentifier auprès de l'Automation API

**Endpoint :** obtenez un access token en envoyant une requête POST à l'endpoint de token.

```
POST https://api.kameleoon.com/oauth/token
```

| Champ          | Type   | Description                                |
| -------------- | ------ | ------------------------------------------ |
| grant\_type    | String | À définir sur `client_credentials`.        |
| client\_id     | String | Votre client ID pour l'Automation API.     |
| client\_secret | String | Votre client secret pour l'Automation API. |

**Exemple :**

```python theme={null}
def kameleoon_token(client_id, client_secret):
    resp = requests.post(
        "https://api.kameleoon.com/oauth/token",
        headers={"Content-Type": "application/x-www-form-urlencoded"},
        data={
            "grant_type": "client_credentials",
            "client_id": client_id,
            "client_secret": client_secret,
        },
    )
    resp.raise_for_status()
    return resp.json()["access_token"]
```

**Réponse :**

```json theme={null}
{ "access_token": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9..." }
```

L'`access_token` retourné est envoyé en tant que token `Bearer` pour chaque requête suivante à l'Automation API. Les access tokens sont valides 2 heures par défaut.

## 2. Récupérer l'expérience

**Endpoint :** récupérez les métadonnées de l'expérience en envoyant une requête GET à l'endpoint [Get an experiment](/api-reference/experiment/get-an-experiment).

```
GET https://api.kameleoon.com/experiments/{experimentId}
```

| Champ        | Type    | Description                                                        |
| ------------ | ------- | ------------------------------------------------------------------ |
| experimentId | Integer | Paramètre de chemin obligatoire. L'ID de l'expérience à récupérer. |

**Exemple :**

```python theme={null}
def get_experiment(token, experiment_id):
    resp = requests.get(
        f"https://api.kameleoon.com/experiments/{experiment_id}",
        headers={"Authorization": f"Bearer {token}"},
    )
    resp.raise_for_status()
    return resp.json()
```

**Réponse (abrégée) :**

```json theme={null}
{
  "id": 188308,
  "name": "Product Page Redesign",
  "status": "STOPPED",
  "dateStarted": "2025-01-15T09:00:00Z",
  "dateEnded": "2025-02-12T18:00:00Z",
  "description": "Testing two redesigns of the product page.",
  "mainGoalId": 279599,
  "variations": [828220, 828221]
}
```

Le script lit `name`, `status`, `dateStarted`, `dateEnded`, et `description` pour la page Notion, ainsi que `mainGoalId` pour restreindre la requête de résultats à l'étape suivante.

<Note>
  L'API retourne `mainGoalId` par défaut, le script n'a donc pas besoin d'un paramètre `optionalFields` pour le lire. L'Automation API ne publie pas d'énumération fixe pour le champ `status`, et les tokens peuvent évoluer. [L'étape 7](#7-mapper-les-données-aux-propriétés-notion) fait correspondre `status` sans distinction de casse, si bien que les différences de casse entre comptes ne cassent pas le mapping.
</Note>

## 3. Demander les résultats de l'expérience

**Endpoint :** déclenchez la génération du rapport de résultats en envoyant une requête POST à l'endpoint [Request experiment's results](/api-reference/experiment/request-experiments-results).

```
POST https://api.kameleoon.com/experiments/{experimentId}/results
```

| Champ                | Type    | Description                                                                                                                                                                                  |
| -------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| experimentId         | Integer | Paramètre de chemin obligatoire.                                                                                                                                                             |
| goalsIds             | Array   | Restreint le rapport aux ID d'objectifs donnés. Le script transmet le `mainGoalId` de l'expérience.                                                                                          |
| referenceVariationId | String  | La variation utilisée comme référence de comparaison. `"0"` utilise la page originale.                                                                                                       |
| visitorData          | Boolean | `false` pour des données par visite, `true` pour des données par visiteur.                                                                                                                   |
| sequentialTesting    | Boolean | À définir sur `true` pour utiliser le sequential testing pour les intervalles de confiance, à la place de la probabilité de succès bayésienne. Activez une méthode ou l'autre, pas les deux. |
| bayesian             | Boolean | À définir sur `true` pour inclure la probabilité de succès bayésienne dans le rapport. Le script lit cette valeur pour renseigner la propriété *Probability*.                                |
| conversionType       | String  | `ALL_CONVERSION` ou `CONVERTED_VISITS`.                                                                                                                                                      |

**Exemple :**

```python theme={null}
def request_results(token, experiment_id, goal_id):
    body = {
        "visitorData": False,
        "sequentialTesting": False,
        "bayesian": True,
        "referenceVariationId": "0",
        "conversionType": "ALL_CONVERSION",
        "goalsIds": [goal_id] if goal_id else None,
    }
    resp = requests.post(
        f"https://api.kameleoon.com/experiments/{experiment_id}/results",
        headers={
            "Authorization": f"Bearer {token}",
            "Content-Type": "application/json",
            "Accept": "*/*",
        },
        json=body,
    )
    resp.raise_for_status()
    return resp.json()["dataCode"]
```

**Réponse :**

```json theme={null}
{ "dataCode": "14443931880924098266207585267983330260134079899081739889989435434342588016765" }
```

Kameleoon génère le rapport de manière asynchrone. L'endpoint retourne un `dataCode` utilisé pour interroger le résultat à l'étape suivante.

<Note>
  Ce script nécessite `bayesian: true` et définit `sequentialTesting: false`. `bayesian` et `sequentialTesting` sont des méthodes alternatives de calcul de la significativité, et ce tutoriel rapporte la probabilité de succès bayésienne. Avec Bayesian activé, la valeur `reliability` du rapport porte la **probabilité de succès bayésienne** (la probabilité qu'une variation batte la référence), que le script mappe sur la propriété *Probability*. Confirmez cette valeur par rapport au même rapport dans l'application Kameleoon si votre compte utilise une méthode statistique par défaut différente.
</Note>

## 4. Interroger les résultats

**Endpoint :** récupérez le rapport en envoyant des requêtes GET à l'endpoint [Poll results](/api-reference/data/poll-results) jusqu'à ce qu'il soit prêt.

```
GET https://api.kameleoon.com/results?dataCode={dataCode}
```

| Champ    | Type   | Description                                                       |
| -------- | ------ | ----------------------------------------------------------------- |
| dataCode | String | Paramètre de requête obligatoire retourné par l'étape précédente. |

Le `status` de la réponse est `WAITING` pendant que le rapport est en cours de calcul, `READY` quand les données sont disponibles, ou `ERROR` / `TIMEOUT` en cas d'échec. Quand le statut est `ERROR` ou `TIMEOUT`, la réponse inclut un `errorDescription` de premier niveau. Le script interroge à intervalle fixe jusqu'à ce que le statut soit `READY`.

**Exemple :**

```python theme={null}
def poll_results(token, data_code, max_attempts=30, delay=2.0):
    for _ in range(max_attempts):
        resp = requests.get(
            "https://api.kameleoon.com/results",
            headers={"Authorization": f"Bearer {token}"},
            params={"dataCode": data_code},
        )
        resp.raise_for_status()
        payload = resp.json()
        status = payload.get("status")
        if status == "READY":
            return payload["data"]
        if status in ("ERROR", "TIMEOUT"):
            raise RuntimeError(payload.get("errorDescription") or status)
        time.sleep(delay)  # status == "WAITING"
    raise TimeoutError("Timed out waiting for results.")
```

**Réponse (abrégée) :**

```json theme={null}
{
  "status": "READY",
  "data": {
    "variationData": {
      "_reference": { "breakdownData": { "_reference": { "generalData": {
        "goalsData": { "279599": { "conversionRate": 0.0149 } } } } } },
      "828220": { "breakdownData": { "_reference": { "generalData": {
        "goalsData": { "279599": {
          "reliability": 100.0,
          "improvementRate": 211.48,
          "conversionRate": 0.0466
        } } } } } },
      "828221": { "breakdownData": { "_reference": { "generalData": {
        "goalsData": { "279599": {
          "reliability": 100.0,
          "improvementRate": -43.33,
          "conversionRate": 0.0085
        } } } } } }
    }
  }
}
```

## 5. Sélectionner la variation la plus performante

Les résultats contiennent une entrée par variation sous `variationData`, ainsi que la ligne `_reference` pour la page originale. Pour chaque variation, les métriques de l'objectif demandé se trouvent sous `breakdownData._reference.generalData.goalsData[goalId]`.

Le script ignore l'entrée `_reference`, lit l'`improvementRate` et la `reliability` (la probabilité de succès bayésienne) de chaque variation, et sélectionne comme meilleure performance la variation ayant le taux d'amélioration le plus élevé. Si le `goalsData` d'une variation ne contient pas l'ID d'objectif demandé, le script se rabat sur l'objectif présent, quel qu'il soit ; comme [l'étape 3](#3-demander-les-résultats-de-l’expérience) restreint déjà la demande à un seul objectif via `goalsIds`, ce repli n'a normalement rien d'autre à sélectionner. La propriété *Result* mappée plus loin indique si cette variation a atteint une probabilité de succès suffisamment élevée avec un uplift positif pour être considérée comme une véritable victoire.

**Exemple :**

```python theme={null}
def pick_best_variation(result_data, goal_id):
    best, best_improvement = {}, float("-inf")
    for variation_id, vdata in result_data["variationData"].items():
        if variation_id == "_reference":
            continue
        general = vdata["breakdownData"]["_reference"]["generalData"]
        goals_data = general.get("goalsData", {})
        if not goals_data:
            continue
        key = str(goal_id) if str(goal_id) in goals_data else next(iter(goals_data))
        metrics = goals_data[key]
        improvement = metrics.get("improvementRate")
        if improvement is not None and improvement > best_improvement:
            best_improvement = improvement
            best = {
                "variation_id": variation_id,
                "improvement_rate": improvement,
                "bayesian_probability": metrics.get("reliability"),
            }
    return best
```

Dans l'exemple, les deux variations atteignent une probabilité de succès bayésienne de 100 %, mais *Redesign 1* (`828220`) affiche une amélioration de +211,48 % contre -43,33 % pour *Redesign 2*. *Redesign 1* est donc la variation la plus performante et, avec une probabilité supérieure à 95 % et un uplift positif, une véritable gagnante.

## 6. Résoudre la source de données Notion

Depuis la version `2025-09-03`, une base de données Notion est un conteneur pour une ou plusieurs **sources de données**, et les écritures et requêtes de pages ciblent un ID de source de données plutôt que l'ID de base de données. Les deux ID ne sont pas interchangeables.

**Endpoint :** récupérez la base de données pour découvrir ses sources de données en envoyant une requête GET à l'endpoint [Retrieve a database](https://developers.notion.com/reference/retrieve-a-database).

```
GET https://api.notion.com/v1/databases/{databaseId}
```

Chaque requête Notion envoie le token d'intégration en tant que token `Bearer`, ainsi que l'en-tête `Notion-Version`.

**Exemple :**

```python theme={null}
NOTION_VERSION = "2025-09-03"


def notion_headers(token):
    return {
        "Authorization": f"Bearer {token}",
        "Notion-Version": NOTION_VERSION,
        "Content-Type": "application/json",
    }


def get_data_source_id(token, database_id):
    resp = requests.get(
        f"https://api.notion.com/v1/databases/{database_id}",
        headers=notion_headers(token),
    )
    resp.raise_for_status()
    data_sources = resp.json().get("data_sources", [])
    if not data_sources:
        raise RuntimeError("The database has no data sources.")
    return data_sources[0]["id"]
```

**Réponse (abrégée) :**

```json theme={null}
{
  "object": "database",
  "id": "255104cd-477e-808c-b279-d39ab803a7d2",
  "data_sources": [
    { "id": "bc1211ca-e3f1-4939-ae34-5260b16f627c", "name": "Experiments" }
  ]
}
```

Le script utilise la première source de données. Si votre base de données en expose plusieurs, choisissez celle dont le schéma correspond aux propriétés *Experiments*.

## 7. Mapper les données aux propriétés Notion

Le script transforme les métadonnées de l'expérience et les métriques de la variation la plus performante en valeurs de propriétés Notion. Chaque type de propriété a sa propre structure JSON.

| Propriété Notion | Type      | Source                                                     | Transformation                                                                                                                                 |
| ---------------- | --------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| Experiment Name  | Title     | `experiment.name`                                          | Direct. Utilisé comme clé d'upsert.                                                                                                            |
| Status           | Select    | `experiment.status`                                        | Mappé, sans distinction de casse : `active → Running`, `draft`/`planned → Implementing`, `stopped`/`diverted → Completed`, `paused → Defunct`. |
| Start date       | Date      | `experiment.dateStarted`                                   | Date-heure tronquée en date ISO (`YYYY-MM-DD`).                                                                                                |
| End date         | Date      | `experiment.dateEnded`                                     | Date-heure tronquée en date ISO.                                                                                                               |
| Notes            | Rich text | `experiment.description`                                   | Direct.                                                                                                                                        |
| Actual           | Number    | `improvementRate` de la meilleure variation                | Direct (uplift mesuré, %).                                                                                                                     |
| Probability      | Select    | probabilité de succès bayésienne de la meilleure variation | Répartie en buckets : `≥95 → 80% - High`, `≥80 → 50% - Medium`, sinon `20% - Low`.                                                             |
| Result           | Select    | probabilité de succès bayésienne + `improvementRate`       | probabilité `≥ 95` et uplift > 0 → `Success` ; probabilité `≥ 95` et uplift \< 0 → `Failure` ; sinon `Inconclusive`.                           |

Le script omet les valeurs vides afin que les valeurs de propriétés existantes ne soient jamais écrasées par des valeurs vides lors d'une mise à jour. Notion crée automatiquement toute option *select* manquante, mais les propriétés elles-mêmes doivent déjà exister dans le schéma de la source de données avec les types corrects.

**Exemple :**

```python theme={null}
STATUS_MAP = {
    "ACTIVE": "Running",
    "DRAFT": "Implementing",
    "PLANNED": "Implementing",
    "PAUSED": "Defunct",
    "STOPPED": "Completed",
    "DIVERTED": "Completed",
}


def map_status(status):
    return STATUS_MAP.get((status or "").upper())


def to_iso_date(value):
    return value[:10] if value else None


def bayesian_to_probability(probability):
    if probability is None:
        return None
    if probability >= 95:
        return "80% - High"
    if probability >= 80:
        return "50% - Medium"
    return "20% - Low"


def derive_result(probability, improvement):
    if probability is None or improvement is None:
        return "Inconclusive"
    if probability >= 95 and improvement > 0:
        return "Success"
    if probability >= 95 and improvement < 0:
        return "Failure"
    return "Inconclusive"


def build_notion_properties(experiment, best):
    probability = best.get("bayesian_probability")
    improvement = best.get("improvement_rate")
    props = {}

    name = experiment.get("name")
    if name:
        props["Experiment Name"] = {"title": [{"text": {"content": name}}]}

    status = map_status(experiment.get("status"))
    if status:
        props["Status"] = {"select": {"name": status}}

    start = to_iso_date(experiment.get("dateStarted"))
    if start:
        props["Start date"] = {"date": {"start": start}}

    end = to_iso_date(experiment.get("dateEnded"))
    if end:
        props["End date"] = {"date": {"start": end}}

    notes = experiment.get("description")
    if notes:
        props["Notes"] = {"rich_text": [{"text": {"content": notes}}]}

    if improvement is not None:
        props["Actual"] = {"number": improvement}

    bucket = bayesian_to_probability(probability)
    if bucket:
        props["Probability"] = {"select": {"name": bucket}}

    result = derive_result(probability, improvement)
    if result:
        props["Result"] = {"select": {"name": result}}

    return props
```

<Note>
  Notion n'autorise qu'une seule propriété de titre par source de données. Le script indexe l'upsert sur la propriété nommée `Experiment Name` ; si votre propriété de titre porte un nom différent, renommez-la ici et dans le filtre de requête à [l'étape 8](#8-upserter-la-page-dans-notion).
</Note>

## 8. Upserter la page dans Notion

Notion n'a pas d'endpoint d'upsert, le script interroge donc la source de données pour trouver une page dont `Experiment Name` correspond, puis la met à jour ou en crée une nouvelle.

**Rechercher :** interrogez la source de données avec un filtre de titre à l'aide de l'endpoint [Query a data source](https://developers.notion.com/reference/query-a-data-source).

```
POST https://api.notion.com/v1/data_sources/{dataSourceId}/query
```

**Créer :** ajoutez une page rattachée à la source de données à l'aide de l'endpoint [Create a page](https://developers.notion.com/reference/post-page).

```
POST https://api.notion.com/v1/pages
```

**Mettre à jour :** écrasez les propriétés de la page correspondante à l'aide de l'endpoint [Update page properties](https://developers.notion.com/reference/patch-page).

```
PATCH https://api.notion.com/v1/pages/{pageId}
```

**Exemple :**

```python theme={null}
def find_page(token, data_source_id, name):
    resp = requests.post(
        f"https://api.notion.com/v1/data_sources/{data_source_id}/query",
        headers=notion_headers(token),
        json={
            "filter": {"property": "Experiment Name", "title": {"equals": name}},
            "page_size": 1,
        },
    )
    resp.raise_for_status()
    results = resp.json().get("results", [])
    return results[0]["id"] if results else None


def upsert_page(token, data_source_id, properties):
    title = properties.get("Experiment Name", {}).get("title")
    name = title[0]["text"]["content"] if title else None

    page_id = find_page(token, data_source_id, name) if name else None
    if page_id:
        resp = requests.patch(
            f"https://api.notion.com/v1/pages/{page_id}",
            headers=notion_headers(token),
            json={"properties": properties},
        )
        action = "Updated"
    else:
        resp = requests.post(
            "https://api.notion.com/v1/pages",
            headers=notion_headers(token),
            json={
                "parent": {"type": "data_source_id", "data_source_id": data_source_id},
                "properties": properties,
            },
        )
        action = "Created"
    resp.raise_for_status()
    return resp.json(), action
```

**Réponse (abrégée) :**

```json theme={null}
{
  "object": "page",
  "id": "1a2b3c4d-5e6f-7081-9abc-def012345678",
  "properties": {
    "Experiment Name": { "title": [{ "plain_text": "Product Page Redesign" }] },
    "Status": { "select": { "name": "Completed" } },
    "Start date": { "date": { "start": "2025-01-15" } },
    "End date": { "date": { "start": "2025-02-12" } },
    "Actual": { "number": 211.48 },
    "Probability": { "select": { "name": "80% - High" } },
    "Result": { "select": { "name": "Success" } }
  }
}
```

## 9. Exécuter le script

Transmettez l'ID de l'expérience ainsi que l'ID de base de données Notion en arguments :

```bash theme={null}
python kameleoon_to_notion.py \
  --experiment-id 188308 \
  --database-id 255104cd477e808cb279d39ab803a7d2
```

Le script affiche chaque étape : l'authentification, l'expérience récupérée, la variation la plus performante, la source de données résolue, les propriétés mappées, et si la page Notion a été créée ou mise à jour.

## Notes de personnalisation

* **Le mapping de statut** se trouve dans la constante `STATUS_MAP`, indexée sur les tokens de statut retournés par l'API et comparée sans distinction de casse. Ajustez les valeurs cibles si vos options *Status* diffèrent de `Running` / `Implementing` / `Completed` / `Defunct`, et confirmez les tokens que votre compte retourne avec un simple `GET /experiments/{experimentId}`.
* **Probability** se mappe à partir de la probabilité de succès bayésienne mesurée, ce qui nécessite `bayesian: true` sur la requête de résultats. Si votre propriété *Probability* est plutôt une estimation pré-expérience que vous saisissez manuellement, supprimez le bloc `Probability` de `build_notion_properties`.
* **La sélection de l'objectif** utilise le `mainGoalId` de l'expérience. Pour établir un rapport sur un objectif différent, transmettez son ID à `request_results` et à `pick_best_variation`.
* **Clé d'upsert.** La correspondance de titre est exacte, donc des différences de casse ou d'espacement dans `Experiment Name` créent une nouvelle page au lieu de mettre à jour celle existante. Le flux de recherche puis d'écriture n'étant pas atomique, évitez d'exécuter deux exports simultanés pour la même expérience.
* **Version de l'API.** Le script fixe `Notion-Version: 2025-09-03`. Si vous ajoutez ultérieurement une deuxième source de données à la base de données, mettez à jour `get_data_source_id` pour sélectionner la bonne par son nom.
* **Limites de débit.** L'Automation API autorise jusqu'à 50 requêtes par 10 secondes et 1 000 par heure ; l'API Notion tolère en moyenne environ 3 requêtes par seconde. Si vous traitez de nombreuses expériences en lot, mettez les tokens en cache et ajoutez une limitation de débit.
