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

# Migrate from Conductor's Legacy Reporting API

> Move your apps and connections from Conductor Intelligence's legacy Reporting API to Conductor's new API before the legacy API reaches end of life on January 15, 2027.

Conductor Intelligence's [legacy Reporting API](https://www.conductor.com/docs/other-apis/conductor-intelligences-legacy-reporting-api.md) (the `/v3/...` endpoints) is being retired. This guide walks you through moving your scripts, data pipelines, and connections to [Conductor's API](https://www.conductor.com/docs/api/introduction.md).

## Overview

### Key dates

| Date | What happens |
| - | - |
| **October 31, 2026** | Deprecation communicated. The migration window begins. |
| **January 15, 2027** | End of life. The legacy Reporting API stops responding. |

> [!WARNING]
>
> After January 15, 2027, calls to the legacy `/v3/...` endpoints will fail. Any report, dashboard, or data pipeline that still depends on them will stop updating.

### Why migrate now

* **Simpler authentication.** A single API token replaces the key, secret, and computed signature.
* **AI Search data.** Get brand mentions, citations, sentiment, responses, and query fan-out alongside your traditional rankings.
* **One set of credentials.** The same token works for the Data API, the Content API, and the configuration endpoints.
* **Actively maintained.** New endpoints and fields ship to Conductor's API only.

## Who needs to act

You need to migrate if anything in your organization calls `https://api.conductor.com/v3/...` with the `apiKey` and `sig` query parameters. Check:

* Custom scripts and data warehouse ETL jobs
* Anything built on the [`conductor-api` Python package](https://pypi.org/project/conductor-api/)
* BI connectors, such as Domo or Looker Studio
* n8n, Zapier, or other automations

> [!NOTE]
>
> The base URL is still `https://api.conductor.com`. Because the host doesn't change, a legacy connection can look up to date when it isn't. Check each integration's **request paths** and **how it authenticates**, not just the host.

## What's different at a glance

| | Legacy Reporting API | Conductor's API |
| - | - | - |
| **Authentication** | API key + secret, with an MD5 signature passed as query parameters (valid within a 5-minute window) | API token passed as a Bearer token in the `Authorization` header |
| **Request style** | `GET` with path parameters (`/v3/{accountId}/...`) | `POST` with a JSON body |
| **Execution** | Synchronous; designed for bulk downloads | Asynchronous: submit, receive an `executionId`, then poll for results |
| **Pagination** | None | `nextPageId` |
| **Response shape** | Named JSON objects | A `schema` array plus positional `results` rows |
| **Date handling** | `timePeriodId` or `CURRENT` | Explicit date ranges aligned to `collection_frequency` |
| **Rate limits** | 5,000 calls/day and 2 calls/second per key | 5,000 calls/day and 2 calls/second per key |
| **Data coverage** | Rankings, search volume, and configuration | Rankings, search volume, configuration, AI Search, and the Content API |

## Before you start

* **Access:** you need a Conductor login to generate an API token. Not a Conductor user? Ask a Conductor admin to add you.
* **An inventory:** list every legacy endpoint you call and where its output goes downstream (tables, dashboards, reports).
* **A parallel-run plan:** keep the legacy connection running alongside the new one until the data matches, then cut over.

## Migrate your integration

**1. Switch authentication**

Generate an API token in Conductor under **Integrations** > **API** > **Create API Token**. Copy it right away; the full token isn't shown again. See [Authentication](https://www.conductor.com/docs/api/authentication.md) for details.

Then remove the signature logic and the `apiKey`/`sig` query parameters, and send the token in the `Authorization` header instead.

```python Before (legacy) theme={null}
import hashlib, time, requests

def gen_api_sig(api_key, secret):
    m = hashlib.md5()
    m.update(api_key.encode())
    m.update(secret.encode())
    m.update(str(int(time.time())).encode())
    return m.hexdigest()

resp = requests.get(
    "https://api.conductor.com/v3/accounts",
    params={"apiKey": API_KEY, "sig": gen_api_sig(API_KEY, SECRET)},
)
```

```python After (Conductor's API) theme={null}
import requests

HEADERS = {
    "Authorization": f"Bearer {CONDUCTOR_API_TOKEN}",
    "Content-Type": "application/json",
}

resp = requests.post(
    "https://api.conductor.com/data-api/v1/async/keyword_rankings",
    headers=HEADERS,
    json={...},  # endpoint-specific parameters
)
```

> [!TIP]
>
> Store the token in a secrets manager or environment variable, not in your code.

> [!NOTE]
>
> Conductor's API still accepts the legacy key + signature method during the transition (see [Support for legacy connections](https://www.conductor.com/docs/api/authentication.md#support-for-legacy-connections)). We recommend switching to a token now so you only migrate once.

**2. Map your endpoints**

Use this table to find the replacement for each legacy endpoint you call.

| Legacy endpoint | Conductor's API endpoint | Notes |
| - | - | - |
| `/v3/accounts` | [`accounts_config`](https://www.conductor.com/docs/api/config-api/endpoints/accounts_config) | |
| `/v3/accounts/{accountId}/web-properties` | [`web_properties_config`](https://www.conductor.com/docs/api/config-api/endpoints/web_properties_config) and [`comparison_web_properties_config`](https://www.conductor.com/docs/api/config-api/endpoints/comparison_web_properties_config) | Comparison web properties are now a separate call. |
| `/v3/accounts/{accountId}/categories` | [`keyword_search_config`](https://www.conductor.com/docs/api/config-api/endpoints/keyword_search_config) | Use `config_type: "keyword_groups"`. Categories are now called keyword groups. |
| `/v3/accounts/{accountId}/web-properties/{webPropertyId}/tracked-searches` | [`keyword_search_config`](https://www.conductor.com/docs/api/config-api/endpoints/keyword_search_config) | Use `config_type: "keywords"`. Tracked searches are now called tracked keywords. |
| `/v3/rank-sources` | [`keyword_search_config`](https://www.conductor.com/docs/api/config-api/endpoints/keyword_search_config) | Use `config_type: "search_engines"`. Rank sources are now called search engines. |
| `/v3/locations` | [`keyword_search_config`](https://www.conductor.com/docs/api/config-api/endpoints/keyword_search_config) | Use `config_type: "locales"`. Locations are now called locales. |
| `/v3/devices` | [`keyword_search_config`](https://www.conductor.com/docs/api/config-api/endpoints/keyword_search_config) | Use `config_type: "devices"`. |
| `/v3/.../serp-items` | [`keyword_rankings`](https://www.conductor.com/docs/api/data-api/endpoints/keyword_rankings) or [`highest_ranking_url_per_keyword`](https://www.conductor.com/docs/api/data-api/endpoints/highest_ranking_url_per_keyword) | Use `highest_ranking_url_per_keyword` if you only need each keyword's top-ranking URL. |
| `/v3/.../search-volumes` | [`keyword_rankings`](https://www.conductor.com/docs/api/data-api/endpoints/keyword_rankings) | Set `includeMsv: true` to return the `approximate_search_volume` column. |

**3. Adopt the async query pattern**

Every Data API and configuration endpoint is asynchronous. Instead of returning data immediately, each query follows a submit, poll, and paginate cycle:

1. **Submit** your query parameters. The API returns `202 Accepted` with an `executionId`.
2. **Poll** by sending the `executionId` back to the same endpoint. You get `202` while the query runs and `200` with data when it's `COMPLETED`.
3. **Paginate** by sending the `executionId` with the `nextPageId` from the previous response, one page at a time, until no `nextPageId` is returned.

```python theme={null}
import time, requests

URL = "https://api.conductor.com/data-api/v1/async/keyword_rankings"
HEADERS = {"Authorization": f"Bearer {CONDUCTOR_API_TOKEN}"}

def run_query(params, poll_interval=5):
    # 1. Submit
    execution_id = requests.post(URL, headers=HEADERS, json=params).json()["executionId"]

    pages, body = [], {"executionId": execution_id}
    while True:
        resp = requests.post(URL, headers=HEADERS, json=body)
        resp.raise_for_status()
        data = resp.json()

        # 2. Poll until the query completes
        if data.get("executionState") != "COMPLETED":
            time.sleep(poll_interval)
            continue

        pages.append(data)

        # 3. Paginate sequentially
        if not data.get("nextPageId"):
            return pages
        body = {"executionId": execution_id, "nextPageId": data["nextPageId"]}
        time.sleep(0.5)  # stay under 2 calls/second
```

See [Async Queries](https://www.conductor.com/docs/api/async-queries.md) for the full walkthrough.

**4. Update your date logic**

Conductor's API doesn't use `timePeriodId` or `CURRENT`. Instead, you send an explicit date range that lines up with the endpoint's `collection_frequency`:

| `collection_frequency` | Start date | End date |
| - | - | - |
| `WEEKLY` | A **Sunday** | A **Saturday** |
| `MONTHLY` | The **first day** of a month | The **last day** of a month |

Requests with misaligned dates are rejected before the query runs. For example, a `WEEKLY` request with `start_date: "2026-10-05"` (a Monday) fails.

To reproduce the legacy `CURRENT` time period, compute the most recent complete week:

```python theme={null}
from datetime import date, timedelta

def last_complete_week(today=None):
    today = today or date.today()
    # Saturday on or before yesterday
    end = today - timedelta(days=(today.weekday() + 2) % 7 or 7)
    start = end - timedelta(days=6)  # the Sunday before
    return start.isoformat(), end.isoformat()
```

> [!NOTE]
>
> Supported frequencies, maximum date ranges, and date field names vary by endpoint. Check each endpoint's reference page before you build your requests.

**5. Update parsing and storage**

Legacy responses were named JSON objects. Conductor's API returns two pieces instead: a `schema` array listing each column's `name` and `type` in order, and `results`, a list of positional rows.

```json theme={null}
{
  "executionState": "COMPLETED",
  "schema": [
    { "name": "keyword", "type": "STRING" },
    { "name": "rank", "type": "INTEGER" }
  ],
  "results": [
    ["running shoes", "3"],
    ["trail shoes", "7"]
  ]
}
```

Build your rows from `schema` rather than hard-coding field positions. This snippet turns a completed response into a pandas DataFrame:

```python theme={null}
import pandas as pd

def to_dataframe(pages):
    columns = [col["name"] for col in pages[0]["schema"]]
    rows = [row for page in pages for row in page["results"]]
    return pd.DataFrame(rows, columns=columns)
```

If you load the data into a warehouse:

* Remap your table columns to the new column names.
* Upsert on the new identifiers rather than the legacy IDs.
* Cast values to the types in `schema`. Some endpoints return all values as strings.

**6. Budget for rate limits**

Limits are the same as before: **5,000 calls per day** and **2 calls per second** per key. What changes is how many calls a job takes, because every poll and every page counts.

The number of calls depends on how many keywords you track. Each keyword returns roughly 30 to 40 rows, and each page holds up to 1,000 rows. For example, a web property with 100 keywords returns about 100 × 40 = 4,000 rows, which is 4,000 ÷ 1,000 = 4 calls to retrieve the data. A weekly sync across 10 web properties of this size takes roughly 10 × 4 = 40 calls.

Use a polling interval of a few seconds rather than polling continuously.

See [Rate Limits](https://www.conductor.com/docs/api/rate-limits.md) for details.

**7. Validate and cut over**

Run the legacy and new connections side by side for one or two collection cycles, then compare:

* Ranks for a sample of keywords
* Comparison web properties

> [!NOTE]
>
> Note that row counts will not match across legacy and new API calls. The legacy endpoint returned only your ranks and your competitors’ ranks, while the new endpoint returns all ranking results for each keyword. To compare like for like, filter the new results down to your web property and its comparison web properties first.

Data rounding and freshness are unchanged, so the numbers should match. Once they do:

1. Point your downstream dashboards and reports at the new data.
2. Turn off the legacy connection.
3. Revoke the legacy API key and secret in Conductor.

## Tool-specific notes

* **`conductor-api` Python package:** this package works only with the legacy API, and no updated version is planned. Replace it with direct HTTP requests, as shown in the examples above.
* **Domo and Looker Studio:** update the connector's authentication and endpoints. See [Integrate Domo with the Conductor API](https://www.conductor.com/docs/other-apis/integrate-domo-with-the-conductor-api.md).
* **No-code tools:** if you use n8n, Zapier, or a similar tool, see [Conductor's API for non-developers](https://www.conductor.com/docs/api/api-non-developers.md).

## Take advantage of what's new

Once you've migrated, the same token gives you access to:

* **AI Search data:** track how your brand appears in AI engines with [brands](https://www.conductor.com/docs/api/data-api/endpoints/ai_search_brands.md), [citations](https://www.conductor.com/docs/api/data-api/endpoints/ai_search_citations.md), [sentiments](https://www.conductor.com/docs/api/data-api/endpoints/ai_search_sentiments.md), [responses](https://www.conductor.com/docs/api/data-api/endpoints/ai_search_response.md), and [query fan-out](https://www.conductor.com/docs/api/data-api/endpoints/ai_search_fan_out.md).
* **The Content API:** create drafts, generate insights, and score content inside your own workflows. See the [Content API guide](https://www.conductor.com/docs/api/content-api-guide.md).

## Troubleshooting and FAQ

> **I get a 401 Unauthorized after switching**
>
> Check that the header is formatted exactly as `Authorization: Bearer YOUR_TOKEN` and that the token hasn't been revoked. If you're unsure, generate a new token.

> **My request is rejected because of the date range**
>
> Your dates don't line up with `collection_frequency`. Weekly ranges must run Sunday through Saturday; monthly ranges must run from the first to the last day of a month.

> **I'm getting 429 Too Many Requests**
>
> You're polling too fast or have used up your daily limit. Increase your polling interval and see [Rate Limits](https://www.conductor.com/docs/api/rate-limits.md).

> **My numbers don't match the legacy data**
>
> Data rounding and freshness haven't changed, so differences usually come from the request itself. Confirm you're querying the same accounts, web properties, search engines, locales, devices, and date range in both connections.

> **Can I keep using my API key and secret?**
>
> Conductor's API still accepts the key + signature method during the transition, but we recommend switching to an API token. The legacy `/v3/...` endpoints stop working on January 15, 2027, regardless of how you authenticate.

> **What happens on January 15, 2027?**
>
> The legacy Reporting API reaches end of life, and calls to the `/v3/...` endpoints fail. Any integration still using them will stop returning data.

## Get help

Questions about your migration? Email [support@conductor.com](mailto:support@conductor.com).
