# Customer Conversations Client API: BI and Analytics Guide

This guide is for a human connecting a BI tool (Looker Studio, Power BI, Tableau, Google
Sheets, or a warehouse job) to the Customer Conversations client API.

This is a living document. It describes exactly what the API does today and is updated as new capabilities ship. Endpoints, fields, or tools not listed here are not available.

## Connection recipe

| Setting | Value |
| --- | --- |
| Base URL | `https://app.customerconversations.co` |
| Authentication | Header `Authorization: Bearer cc_...` on every request |
| Records | JSON, under the `data` key of every response |
| Pagination | `limit` (max 200) + `starting_after` (pass the previous page's `meta.next_cursor`) on every list endpoint |
| Incremental pulls | `updated_since` on campaigns and synthesis; interviews has no update timestamp, use `date_from`/`date_to` on `created_at` |
| Rate limit | 600 requests per hour per key; `429` with `Retry-After` beyond that |
| Machine contract | `https://app.customerconversations.co/api/client/v1/openapi.json` |

The list endpoints you can pull from today are `https://app.customerconversations.co/api/client/v1/interviews`,
`https://app.customerconversations.co/api/client/v1/campaigns`, and `https://app.customerconversations.co/api/client/v1/synthesis`. Interview rows carry the
metric-ready columns `nps_score`, `customer_sentiment`, `churn_risk`,
`duration_seconds`, `conversation_goal_id` (the campaign), `status`, and
`created_at`.

**Honest note on aggregates:** endpoints that return pre-aggregated metrics (interview
volume, sentiment, or NPS grouped by campaign) are not yet available. Today you derive
metrics in your BI tool from the raw `/interviews` rows, joined to `/campaigns` on
`conversation_goal_id`. Because the interviews endpoint applies the same usable-interview
rules as the portal, aggregates you build from it reconcile with portal numbers. This guide
is updated as new capabilities ship.

**Key safety:** treat the API key like a password. Load it from a data-source credential
setting, an environment variable, or a secrets manager. Never embed it in a shared
dashboard formula, a published report, or a spreadsheet cell viewers can read.

## Google Sheets (Apps Script)

Extensions > Apps Script, then paste and run `syncInterviews`. Store the key in Script
Properties (Project Settings > Script Properties, property name `CC_API_KEY`), not in the
code.

```javascript
function syncInterviews() {
  var key = PropertiesService.getScriptProperties().getProperty('CC_API_KEY');
  var base = 'https://app.customerconversations.co/api/client/v1/interviews?limit=200';
  var headers = { Authorization: 'Bearer ' + key };
  var rows = [['id', 'campaign_id', 'status', 'nps_score', 'customer_sentiment', 'churn_risk', 'duration_seconds', 'created_at']];
  var url = base;
  while (url) {
    var resp = UrlFetchApp.fetch(url, { headers: headers });
    var body = JSON.parse(resp.getContentText());
    body.data.forEach(function (iv) {
      rows.push([iv.id, iv.conversation_goal_id, iv.status, iv.nps_score, iv.customer_sentiment, iv.churn_risk, iv.duration_seconds, iv.created_at]);
    });
    url = body.meta.has_more
      ? base + '&starting_after=' + encodeURIComponent(body.meta.next_cursor)
      : null;
  }
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('interviews')
    || SpreadsheetApp.getActiveSpreadsheet().insertSheet('interviews');
  sheet.clearContents();
  sheet.getRange(1, 1, rows.length, rows[0].length).setValues(rows);
}
```

Add a time-driven trigger (for example hourly) to keep the sheet fresh. A full re-pull of a
few thousand interviews costs only dozens of requests, well inside the hourly budget.

## Looker Studio

Looker Studio's built-in URL connectors cannot send an `Authorization` header, so do not
paste the API URL directly. Two working patterns:

1. **Via Google Sheets (simplest):** run the Apps Script sync above, then connect Looker
   Studio to the sheet with the standard Google Sheets connector. The trigger keeps it
   fresh.
2. **Community connector (direct):** build a small Apps Script community connector whose
   `getData` fetches `https://app.customerconversations.co/api/client/v1/interviews` with the Bearer header (same pagination loop as
   above) and store the key with the connector's credentials support, so it never appears
   in the report.

## Power BI

Get Data > Blank Query, then paste into the Advanced Editor. Parameterize the key (Manage
Parameters) rather than leaving it inline, and set the data source privacy level to
Organizational.

```
let
    BaseUrl = "https://app.customerconversations.co/api/client/v1",
    ApiKey = ApiKeyParameter,
    GetPage = (cursor as nullable text) =>
        let
            Query = if cursor = null then [limit = "200"] else [limit = "200", starting_after = cursor],
            Response = Json.Document(
                Web.Contents(BaseUrl, [
                    RelativePath = "interviews",
                    Query = Query,
                    Headers = [Authorization = "Bearer " & ApiKey]
                ])
            )
        in
            Response,
    AllPages = List.Generate(
        () => GetPage(null),
        each _ <> null,
        each if [meta][has_more] then GetPage([meta][next_cursor]) else null
    ),
    Rows = List.Combine(List.Transform(AllPages, each [data])),
    Table = Table.FromRecords(Rows)
in
    Table
```

Swap `interviews` for `campaigns` or `synthesis` to pull the other resources; those
two also accept an `updated_since` query field for incremental refresh.

## Tableau

Tableau has no native header-auth REST connector, so use one of:

1. **CSV extract (simplest):** run the Python script below on a schedule, point Tableau at
   the CSV (or the warehouse table), and refresh the extract.
2. **Web Data Connector:** host a small WDC page whose JavaScript fetches
   `https://app.customerconversations.co/api/client/v1/interviews` with the Bearer header and the standard pagination loop, and
   collect the key through the WDC's own auth UI so it stays out of the workbook.

## Python sync (CSV or warehouse)

Works as a cron job feeding any warehouse or a CSV for Tableau/Excel. Reads the key from
the environment.

```python
import csv, os, time
import requests

BASE = "https://app.customerconversations.co/api/client/v1"
HEADERS = {"Authorization": "Bearer " + os.environ["CC_API_KEY"]}

def walk(path, **filters):
    params = {"limit": 200, **filters}
    while True:
        resp = requests.get(BASE + path, headers=HEADERS, params=params)
        if resp.status_code == 429:
            time.sleep(int(resp.headers.get("Retry-After", "60")))
            continue
        resp.raise_for_status()
        body = resp.json()
        yield from body["data"]
        if not body["meta"].get("has_more"):
            return
        params["starting_after"] = body["meta"]["next_cursor"]

# Full pull of interviews (no update timestamp on this resource, so either
# re-pull fully or window on created_at with date_from/date_to):
FIELDS = ["id", "conversation_goal_id", "status", "nps_score",
          "customer_sentiment", "churn_risk", "duration_seconds", "created_at"]
with open("interviews.csv", "w", newline="") as f:
    writer = csv.DictWriter(f, fieldnames=FIELDS, extrasaction="ignore")
    writer.writeheader()
    for iv in walk("/interviews"):
        writer.writerow(iv)

# Incremental pull of campaigns and synthesis via updated_since (store the
# high-water mark between runs):
since = os.environ.get("CC_SYNC_SINCE", "1970-01-01T00:00:00Z")
campaigns = list(walk("/campaigns", updated_since=since))
synthesis = list(walk("/synthesis", updated_since=since))
```

## MCP connection for analyst agents

Claude Code and other MCP Streamable HTTP clients can connect to `https://app.customerconversations.co/api/client/v1/mcp` with the
same `Authorization: Bearer cc_...` header. The read-only tool surface is scope-aware and
delegates to these same REST readers. Use the exact `.mcp.json` configuration in the agent
guide at `https://app.customerconversations.co/api/client/v1/agent-guide.md`.

## Reference

Full endpoint semantics, filters, error codes, and the pagination contract live in the
OpenAPI document at `https://app.customerconversations.co/api/client/v1/openapi.json` and in the agent guide at
`https://app.customerconversations.co/api/client/v1/agent-guide.md` (both public). Questions or a tool this guide does not cover:
tell your Customer Conversations contact and this guide will grow to match.
