> ## 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.

# Exportar los resultados de experimentos a Notion

> Recupere un experimento y sus resultados con la Automation API, determine la variación con mejor rendimiento a partir de la probabilidad de éxito bayesiana, e inserte o actualice el registro en una base de datos de Notion con un único script de Python.

Recupere un experimento y sus resultados con la Automation API, transfórmelos en propiedades de página de Notion e inserte o actualice el registro en una base de datos de Notion usando un único script de Python.

## Objetivo

Este tutorial describe cómo funciona el script [kameleoon\_to\_notion.py](/assets/developer-docs/script/apis/automation-api-rest/tutorials/kameleoon_to_notion.py.zip), paso a paso. Dado un **ID de experimento** de Kameleoon y un **ID de base de datos** de Notion, el script recupera los metadatos y los resultados estadísticos del experimento, determina la variación con mejor rendimiento, asigna los datos a una base de datos *Experiments* de Notion y escribe el registro en Notion. Volver a ejecutar el script para el mismo experimento actualiza la página existente en lugar de crear un duplicado.

Los pasos 1 a 5 reutilizan el mismo flujo de solicitud y consulta que el [tutorial de exportación a Airtable](./exporting-experiment-results-to-airtable); solo cambia el destino.

<Note>
  La API de Notion no tiene una función de upsert nativa. El script emula una: consulta la fuente de datos de la base de datos en busca de una página cuyo título coincida con el nombre del experimento, y luego actualiza esa página si la encuentra o crea una nueva en caso contrario. Este tutorial utiliza la versión `2025-09-03` de la API de Notion, que organiza cada base de datos alrededor de una o varias **fuentes de datos**.
</Note>

## Requisitos

* **Credenciales de la API de Kameleoon.** La Automation API requiere un token de acceso. El script obtiene uno de forma programática a partir de un `client_id` y un `client_secret` mediante el grant `client_credentials`. Consulte [Obtener un token de acceso](/es/developer-docs/apis/automation-api-rest/get-started/get-started#1-obtener-un-access-token).

* **Un token de integración interna de Notion.** Cree una integración en [notion.so/my-integrations](https://www.notion.so/my-integrations) y copie su token.

* **Una base de datos de Notion** con un esquema *Experiments* y las siguientes propiedades: `Experiment Name` (title), `Status` (select), `Start date` (date), `End date` (date), `Notes` (rich text), `Actual` (number), `Probability` (select) y `Result` (select).

* **El ID de la base de datos de Notion.** Abra la base de datos como página completa (el ID es la cadena de 32 caracteres en su URL, antes del parámetro de vista `?v=`).

* **Python 3.9 o superior** con la biblioteca `requests` (`pip install requests`).

<Warning>
  Comparta la base de datos con su integración, o todas las solicitudes devuelven `object_not_found`. Abra la base de datos, vaya a **`•••` → Connections → Add connections** y seleccione su integración.
</Warning>

<Warning>
  Almacene todas las credenciales en variables de entorno. Nunca codifique secretos directamente en el script.
</Warning>

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

El tutorial utiliza el experimento de ejemplo **Product Page Redesign** (ID `188308`), con dos variaciones además de la original: *Redesign 1* (ID `828220`) y *Redesign 2* (ID `828221`).

## 1. Autenticarse con la Automation API

**Endpoint:** Obtenga un token de acceso enviando una solicitud POST al endpoint de token.

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

| Campo          | Tipo   | Descripción                                 |
| -------------- | ------ | ------------------------------------------- |
| grant\_type    | String | Establézcalo en `client_credentials`.       |
| client\_id     | String | Su ID de cliente de la Automation API.      |
| client\_secret | String | Su secreto de cliente de la Automation API. |

**Ejemplo:**

```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"]
```

**Respuesta:**

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

El `access_token` devuelto se envía como token `Bearer` en cada solicitud posterior a la Automation API. Los tokens de acceso son válidos durante 2 horas de forma predeterminada.

## 2. Recuperar el experimento

**Endpoint:** Obtenga los metadatos del experimento enviando una solicitud GET al endpoint [Get an experiment](/api-reference/experiment/get-an-experiment).

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

| Campo        | Tipo    | Descripción                                                                 |
| ------------ | ------- | --------------------------------------------------------------------------- |
| experimentId | Integer | Parámetro de ruta obligatorio. El ID del experimento que se va a recuperar. |

**Ejemplo:**

```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()
```

**Respuesta (truncada):**

```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]
}
```

El script lee `name`, `status`, `dateStarted`, `dateEnded` y `description` para la página de Notion, y `mainGoalId` para acotar la solicitud de resultados en el siguiente paso.

<Note>
  La API devuelve `mainGoalId` de forma predeterminada, por lo que el script no necesita un parámetro `optionalFields` para leerlo. La Automation API no publica una enumeración fija para el campo `status`, y los tokens pueden evolucionar. El [paso 7](#7-asignar-los-datos-a-las-propiedades-de-notion) compara `status` sin distinguir mayúsculas y minúsculas, de modo que las diferencias de mayúsculas y minúsculas entre cuentas no rompen la asignación.
</Note>

## 3. Solicitar los resultados del experimento

**Endpoint:** Active la generación del informe de resultados enviando una solicitud POST al endpoint [Request experiment's results](/api-reference/experiment/request-experiments-results).

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

| Campo                | Tipo    | Descripción                                                                                                                                                             |
| -------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| experimentId         | Integer | Parámetro de ruta obligatorio.                                                                                                                                          |
| goalsIds             | Array   | Restringe el informe a los IDs de objetivo indicados. El script pasa el `mainGoalId` del experimento.                                                                   |
| referenceVariationId | String  | La variación utilizada como referencia para la comparación. `"0"` utiliza la página original.                                                                           |
| visitorData          | Boolean | `false` para datos basados en visitas, `true` para datos basados en visitantes.                                                                                         |
| sequentialTesting    | Boolean | Establézcalo en `true` para usar pruebas secuenciales en los intervalos de confianza en lugar de la probabilidad de éxito bayesiana. Active un método u otro, no ambos. |
| bayesian             | Boolean | Establézcalo en `true` para incluir la probabilidad de éxito bayesiana en el informe. El script lee este valor para completar la propiedad *Probability*.               |
| conversionType       | String  | `ALL_CONVERSION` o `CONVERTED_VISITS`.                                                                                                                                  |

**Ejemplo:**

```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"]
```

**Respuesta:**

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

Kameleoon genera el informe de forma asíncrona. El endpoint devuelve un `dataCode` que se utiliza para consultar el resultado en el siguiente paso.

<Note>
  Este script requiere `bayesian: true` y establece `sequentialTesting: false`. `bayesian` y `sequentialTesting` son métodos alternativos para calcular la significancia, y este tutorial reporta la probabilidad de éxito bayesiana. Con Bayesian habilitado, el valor `reliability` del informe representa la **probabilidad de éxito bayesiana** (la probabilidad de que una variación supere a la referencia), que el script asigna a la propiedad *Probability*. Confirme el valor con el mismo informe en la aplicación Kameleoon si su cuenta utiliza un método estadístico predeterminado diferente.
</Note>

## 4. Consultar los resultados

**Endpoint:** Recupere el informe enviando solicitudes GET al endpoint [Poll results](/api-reference/data/poll-results) hasta que esté listo.

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

| Campo    | Tipo   | Descripción                                                      |
| -------- | ------ | ---------------------------------------------------------------- |
| dataCode | String | Parámetro de consulta obligatorio devuelto por el paso anterior. |

El `status` de la respuesta es `WAITING` mientras se calcula el informe, `READY` cuando los datos están disponibles, o `ERROR` / `TIMEOUT` en caso de fallo. Cuando el estado es `ERROR` o `TIMEOUT`, la respuesta incluye un `errorDescription` de nivel superior. El script consulta a intervalos fijos hasta que el estado es `READY`.

**Ejemplo:**

```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.")
```

**Respuesta (truncada):**

```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. Seleccionar la variación con mejor rendimiento

Los resultados contienen una entrada por variación bajo `variationData`, además de la línea `_reference` para la página original. Para cada variación, las métricas del objetivo solicitado se encuentran bajo `breakdownData._reference.generalData.goalsData[goalId]`.

El script omite la entrada `_reference`, lee el `improvementRate` y el `reliability` (la probabilidad de éxito bayesiana) de cada variación, y selecciona como mejor variación la que tiene la tasa de mejora más alta. Si el `goalsData` de una variación no contiene el ID de objetivo solicitado, el script recurre al objetivo que esté presente, sea cual sea; como el [paso 3](#3-solicitar-los-resultados-del-experimento) ya limita la solicitud a un único objetivo mediante `goalsIds`, normalmente este recurso no tiene ningún otro objetivo entre el que elegir. La propiedad *Result*, asignada más adelante, registra si esa variación alcanzó una probabilidad de éxito suficientemente alta con una mejora positiva como para contar como una victoria genuina.

**Ejemplo:**

```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
```

En el ejemplo, ambas variaciones alcanzan una probabilidad de éxito bayesiana del 100%, pero *Redesign 1* (`828220`) muestra una mejora del +211,48% frente al -43,33% de *Redesign 2*. Por lo tanto, *Redesign 1* es la variación con mejor rendimiento y, con una probabilidad superior al 95% y una mejora positiva, una ganadora genuina.

## 6. Resolver la fuente de datos de Notion

Desde la versión `2025-09-03`, una base de datos de Notion es un contenedor para una o varias **fuentes de datos**, y las escrituras y consultas de páginas se dirigen a un ID de fuente de datos en lugar del ID de la base de datos. Los dos ID no son intercambiables.

**Endpoint:** Recupere la base de datos para descubrir sus fuentes de datos enviando una solicitud GET al endpoint [Retrieve a database](https://developers.notion.com/reference/retrieve-a-database).

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

Cada solicitud a Notion envía el token de integración como token `Bearer` y el encabezado `Notion-Version`.

**Ejemplo:**

```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"]
```

**Respuesta (truncada):**

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

El script utiliza la primera fuente de datos. Si su base de datos expone varias, elija la que tenga el esquema que coincide con las propiedades de *Experiments*.

## 7. Asignar los datos a las propiedades de Notion

El script transforma los metadatos del experimento y las métricas de la variación con mejor rendimiento en valores de propiedad de Notion. Cada tipo de propiedad tiene su propia estructura JSON.

| Propiedad de Notion | Tipo      | Origen                                                | Transformación                                                                                                                                               |
| ------------------- | --------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Experiment Name     | Title     | `experiment.name`                                     | Directo. Se utiliza como clave de upsert.                                                                                                                    |
| Status              | Select    | `experiment.status`                                   | Asignado sin distinguir mayúsculas y minúsculas: `active → Running`, `draft`/`planned → Implementing`, `stopped`/`diverted → Completed`, `paused → Defunct`. |
| Start date          | Date      | `experiment.dateStarted`                              | Fecha y hora truncadas a una fecha ISO (`YYYY-MM-DD`).                                                                                                       |
| End date            | Date      | `experiment.dateEnded`                                | Fecha y hora truncadas a una fecha ISO.                                                                                                                      |
| Notes               | Rich text | `experiment.description`                              | Directo.                                                                                                                                                     |
| Actual              | Number    | `improvementRate` de la mejor variación               | Directo (mejora medida, %).                                                                                                                                  |
| Probability         | Select    | probabilidad de éxito bayesiana de la mejor variación | Agrupado por rangos: `≥95 → 80% - High`, `≥80 → 50% - Medium`, en otro caso `20% - Low`.                                                                     |
| Result              | Select    | probabilidad de éxito bayesiana + `improvementRate`   | probabilidad `≥ 95` y mejora > 0 → `Success`; `≥ 95` y mejora \< 0 → `Failure`; en cualquier otro caso → `Inconclusive`.                                     |

El script omite los valores vacíos, de modo que los valores de propiedad existentes nunca se sobrescriben con valores en blanco al actualizar. Notion crea automáticamente las opciones de *select* que falten, pero las propiedades en sí deben existir ya en el esquema de la fuente de datos con los tipos correctos.

**Ejemplo:**

```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 permite solo una propiedad de título por fuente de datos. El script basa el upsert en la propiedad llamada `Experiment Name`; si su propiedad de título tiene un nombre diferente, cámbielo aquí y en el filtro de consulta del [paso 8](#8-insertar-o-actualizar-la-página-en-notion).
</Note>

## 8. Insertar o actualizar la página en Notion

Notion no tiene un endpoint de upsert, por lo que el script consulta la fuente de datos en busca de una página cuyo `Experiment Name` coincida, y luego la actualiza o crea una nueva.

**Buscar:** Consulte la fuente de datos con un filtro de título usando el endpoint [Query a data source](https://developers.notion.com/reference/query-a-data-source).

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

**Crear:** Añada una página cuyo elemento superior sea la fuente de datos usando el endpoint [Create a page](https://developers.notion.com/reference/post-page).

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

**Actualizar:** Sobrescriba las propiedades de la página encontrada usando el endpoint [Update page properties](https://developers.notion.com/reference/patch-page).

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

**Ejemplo:**

```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
```

**Respuesta (truncada):**

```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. Ejecutar el script

Pase el ID del experimento y el ID de la base de datos de Notion como argumentos:

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

El script imprime cada paso: la autenticación, el experimento obtenido, la variación con mejor rendimiento, la fuente de datos resuelta, las propiedades asignadas y si la página de Notion se creó o se actualizó.

## Notas de personalización

* **La asignación de estado** vive en la constante `STATUS_MAP`, indexada por los tokens de estado que devuelve la API y comparada sin distinguir mayúsculas y minúsculas. Ajuste los valores de destino si sus opciones de *Status* difieren de `Running` / `Implementing` / `Completed` / `Defunct`, y confirme los tokens que devuelve su cuenta con una única solicitud `GET /experiments/{experimentId}`.
* **Probability** se asigna a partir de la probabilidad de éxito bayesiana medida, que requiere `bayesian: true` en la solicitud de resultados. Si su propiedad *Probability* es en cambio una estimación previa al experimento que introduce manualmente, elimine el bloque `Probability` de `build_notion_properties`.
* **La selección de objetivo** usa el `mainGoalId` del experimento. Para generar el informe sobre un objetivo diferente, pase su ID a `request_results` y `pick_best_variation`.
* **Clave de upsert.** La coincidencia de título es exacta, por lo que las diferencias de mayúsculas/minúsculas o de espacios en `Experiment Name` crean una página nueva en lugar de actualizar la existente. Como el flujo de buscar y luego escribir no es atómico, evite ejecutar dos exportaciones para el mismo experimento de forma simultánea.
* **Versión de la API.** El script fija `Notion-Version: 2025-09-03`. Si más adelante añade una segunda fuente de datos a la base de datos, actualice `get_data_source_id` para seleccionar la correcta por nombre.
* **Límites de frecuencia.** La Automation API permite hasta 50 solicitudes cada 10 segundos y 1000 por hora; la API de Notion promedia unas 3 solicitudes por segundo. Si procesa muchos experimentos por lotes, almacene en caché los tokens y añada limitación de frecuencia.
