Goal
This tutorial describes how the kameleoon_to_confluence.py script works, step by step. Given a Kameleoon experiment ID, a Confluence site, and a Confluence space ID, the script retrieves the experiment metadata and statistical results, identifies the winning variation, and upserts a row for that experiment into a table on a Confluence page. Re-running the script for the same experiment updates its existing row instead of adding a duplicate. Steps 1–5 reuse the same request and poll flow as the Airtable export tutorial. Only the destination changes.Alternative: export with an AI assistant instead of the script
Running the script requires a Python environment, four stored credentials, and a manual re-run for every experiment you want to export. If you already use an MCP-compatible AI assistant such as Claude, you can perform the same export from a conversation instead, by connecting it to both Kameleoon’s and Atlassian’s own remote MCP (Model Context Protocol) servers, either through a coding tool or directly in the Claude app. Follow the guide to export results using Claude.Requirements
-
Kameleoon API credentials. The Automation API requires an access token. The script obtains one programmatically from a
client_idandclient_secretusing theclient_credentialsgrant. See Obtain an access token. - A Confluence API token, paired with the email address of the account it belongs to. Create a token at id.atlassian.com/manage-profile/security/api-tokens. The script sends both as HTTP Basic authentication on every Confluence request.
- The numeric Confluence space ID for the space that hosts the page. Unlike a Notion database ID or an Airtable base ID, a space’s numeric ID doesn’t appear in its URL (that URL shows the space key instead). Find it under Space settings in Confluence, or ask an MCP-connected AI assistant to look it up for you.
-
Python 3.9+ with the
requestslibrary (pip install requests).
188308), with two variations in addition to the original: variation 828220 and variation 828221.
1. Authenticate with the Automation API
Endpoint: obtain an access token by sending a POST request to the token endpoint.
Example:
access_token as a Bearer token on every subsequent Automation API request. Access tokens remain valid for 2 hours by default.
2. Retrieve the experiment
Endpoint: fetch the experiment metadata by sending a GET request to the Get an experiment endpoint.
Example:
name, status, dateStarted, dateEnded, and description for the Confluence row, and mainGoalId to scope the results request in the next step.
The API returns
mainGoalId by default, so you don’t need an optionalFields parameter to read it. The Automation API doesn’t publish a fixed enum for the status field, but other status-type fields across the API consistently use uppercase tokens (for example, STOPPED, ACTIVE, DRAFT). Step 6 maps the status field on that assumption. Confirm the exact tokens your account returns with a live request before you rely on the mapping.3. Request the experiment’s results
Endpoint: trigger the generation of the results report by sending a POST request to the Request experiment’s results endpoint.
Example:
dataCode, which step 4 uses to poll for the result.
This script requires
bayesian: true and sets sequentialTesting: false. bayesian and sequentialTesting are alternative methods for computing significance, and this tutorial reports the Bayesian success probability. With Bayesian enabled, the report’s reliability value carries the Bayesian success probability (the probability that a variation beats the reference), which the script maps to the Probability column. Confirm the value against the same report in the Kameleoon app if your account uses a different default statistical method.4. Poll for the results
Endpoint: retrieve the report by sending GET requests to the Poll results endpoint until it’s ready.
The response
status is WAITING while Kameleoon computes the report, READY when the data is available, or ERROR / TIMEOUT on failure. When the status is ERROR or TIMEOUT, the response includes a top-level errorDescription. The script polls on a fixed interval until the status is READY.
Example:
5. Select the winning variation
The results contain one entry per variation undervariationData, plus the _reference line for the original page. For each variation, the metrics for the requested goal sit under breakdownData._reference.generalData.goalsData[goalId].
The script skips the _reference entry, reads each variation’s improvementRate and reliability (the Bayesian success probability), and selects the variation with the highest improvement rate as the winner. The Result column mapped in the next step records whether that variation actually won: whether it reached a high enough success probability with a positive uplift.
Example:
828220 shows a +211.48% improvement against variation 828221’s -43.33%. Variation 828220 therefore has the higher improvement rate and, with a probability above 95% and a positive uplift, is the genuine winner.
6. Map the data to Confluence columns
The script transforms the experiment metadata and the winning variation’s metrics into a row for the Confluence table, using the same eight columns as the Notion and Airtable exports.
Example:
The Automation API doesn’t publish a fixed enum for the experiment
status field, and the tokens can evolve. map_status matches case-insensitively and returns None for an unrecognized status, which drops the Status cell rather than writing a wrong value. Confirm the tokens your account returns with a single GET /experiments/{experimentId} and extend STATUS_MAP if needed.7. Parse the existing Experiments table
Confluence has no database or table object of its own. Instead, the script keeps one dedicated page with a single HTML table on it, one row per experiment, and treats the Experiment Name column the same way Notion treats a title property or Airtable treats a merge field: as the key it upserts on. The script builds that table with afull-width layout rather than Confluence’s narrower default, since 8 columns of moderately long headers wrap mid-word at the default page width.
Confluence’s storage format (the HTML-like markup a page’s body is stored as) wraps every cell’s text in a <p> tag, and a freshly created page’s header row is plain <tr><th>...</th></tr> cells with no surrounding <thead>. The parser below, built on Python’s standard html.parser.HTMLParser, handles both that bare-header case and a <thead>-wrapped one, and treats an empty cell the same whether Confluence renders it as an empty <p></p> or a self-closing <p />. It also stops at the first </table>, so a second table elsewhere on the page (one you added by hand, for example) never merges into the parsed result, and closes any row or cell that a malformed edit left open before starting the next one, so a stray unclosed tag can’t silently drop a row.
Example:
reorder_row guards against a table whose columns were manually reordered in Confluence, for example by a person dragging a column in the editor. Without it, a lookup by position would compare the new row’s Experiment Name against whatever column now sits in that position on the existing table, silently matching the wrong row or none at all.8. Find, create, or update the Confluence page
Find: search for the page by title within the space using thetitle and space-id query parameters on the Get pages endpoint. Passing body-format=storage returns the current table content in the same call.
Confluence has no per-row or per-field update endpoint. Updating a page replaces its entire body, so the script always reads the current table, upserts one row in memory, and writes the whole table back. That whole-body replacement differs from the Airtable export, which patches a single record, and the Notion export, which patches a single page’s properties.
Unlike the Notion and Airtable APIs, Confluence requires the caller to increment
version.number on every update, to the current version number plus one. Sending the current number again, or omitting version, causes the request to fail. An MCP-connected AI assistant using Atlassian’s own tools handles this automatically; a direct REST call, as in this script, doesn’t.9. Run the script
Pass the experiment ID, the Confluence site, and the space ID as arguments:--page-title to target a page name other than the default Experiments.

Full script
The complete script below matches kameleoon_to_confluence.py function for function. Copy it directly, or download the file from that link.Customization notes
- Status mapping lives in the
STATUS_MAPconstant, keyed on the uppercase status tokens the API returns. Adjust the target values if your Status labels differ fromRunning/Implementing/Completed/Defunct, and confirm the tokens your account returns with a liveGET /experiments/{experimentId}request before you rely on the mapping. - Probability comes from the measured Bayesian success probability, which requires
bayesian: trueon the results request. If you want a pre-experiment estimate instead of a measured one, remove theProbabilityline frombuild_row. - Goal selection uses the experiment’s
mainGoalId. To report on a different goal, pass its ID torequest_resultsandpick_best_variation. - Upsert key. The match on Experiment Name is exact, so differences in case or whitespace create a new row instead of updating the existing one. Because the find-then-write flow isn’t atomic, avoid running two exports for the same experiment concurrently, and avoid renaming an experiment between runs unless you also want a new row for it.
- Whole-page updates. Every update rewrites the entire table, since Confluence has no per-row write. If you maintain the page by hand between script runs, keep your edits inside the table the script builds. Content outside that table isn’t currently preserved.
- Version conflicts. The script always reads the page’s current
version.numberimmediately before writing, so a manual edit made between the read and the write causes the nextPUTto fail with a version conflict. Re-run the script if that happens. - Rate limits. The Automation API allows up to 50 requests per 10 seconds and 1,000 per hour, but Kameleoon recommends staying under 12 calls per minute per account. Confluence Cloud enforces its own rate limits, which vary by plan; see Atlassian’s rate limiting documentation for current values. If you batch many experiments, cache tokens, throttle requests, and consider the Data API for high-volume needs.