---
title: "Customer Data API — Developer Documentation"
slug: "/help/api/customer-data-api"
last_updated: "2026-03-10T09:03:36Z"
zendesk_id: 49763637906707
zendesk_url: "https://help.walnut.io/hc/en-us/articles/49763637906707-Customer-Data-API-Developer-Documentation"
locale: "en-us"
product: "api"
displayed_sidebar: "apiSidebar"
sidebar_position: 4
---

# Overview

The Walnut Customer Data API gives you programmatic, self-service access to your demo analytics data. Query session-level insights, retrieve engagement metrics, and feed data directly into your CRM, BI tools, internal dashboards, or AI-powered workflows.

This initial release exposes **demo session data**. Additional endpoints (demo metadata, playlist sessions, admin analytics) will be introduced in future releases.

## Key Capabilities

-   Query individual demo sessions with filtering, pagination, and field selection
-   Aggregate session data by date, user type, viewer type, demo, or embed status
-   Retrieve pre-built analytics summaries with totals, averages, and top-10 breakdowns
-   Integrate demo data into any system that can make HTTP requests

---

# Authentication

All API requests (except the health check endpoint) require authentication via an API key passed in the `x-api-key` request header.

## Making an Authenticated Request

```bash
curl -H "x-api-key: YOUR_API_KEY" \\
  "<https://customer-api.teamwalnut.com/demo-sessions?limit=10>"
```

> ⚠️ Your API key is provided once by the Walnut team and cannot be recovered if lost. Store it securely. If you need a new key, contact your Walnut account representative.

## Rate Limits

The API enforces the following rate limits per API key:

| Limit | Value |
| --- | --- |
| Sustained throughput | 50 requests/second |
| Burst allowance | 100 requests/second |
| Max rows per request | 10,000 |

Requests exceeding the rate limit will receive a `429 Too Many Requests` response. Implement exponential backoff in your integration.

---

# Endpoints

| Path | Method | Auth | Description |
| --- | --- | --- | --- |
| `/health` | GET | None | Health check |
| `/demo-sessions` | GET | `x-api-key` | Query demo sessions with filtering, pagination, field selection, and aggregation |
| `/demo-sessions/summary` | GET | `x-api-key` | Pre-built analytics summary (totals, averages, top demos, top countries) |

---

## GET /demo-sessions

Retrieve demo session records with support for filtering, pagination, field selection, and aggregation.

### Query Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `fields` | string | (all fields) | Comma-separated list of fields to return |
| `limit` | integer | 1,000 | Max rows to return (maximum: 10,000) |
| `offset` | integer | 0 | Pagination offset |
| `start_date` | YYYY-MM-DD | (none) | Include sessions from this date (inclusive) |
| `end_date` | YYYY-MM-DD | (none) | Include sessions up to this date (inclusive) |
| `user_type` | string | all | Filter by user type: "external", "internal", or "all" |
| `demo_id` | UUID | (none) | Filter sessions for a specific demo |
| `group_by` | string | (none) | Aggregate results by: date, user\_type, viewer\_type, demo\_id, is\_embed |

### Available Fields (29)

Use the `fields` parameter to select only the data you need. If omitted, all fields are returned.

| Category | Fields |
| --- | --- |
| Identifiers | `id`, `demo_id`, `company_id` |
| Classification | `viewer_type`, `user_type` |
| Timing | `started_at`, `finished_at` |
| Session State | `is_bounced`, `is_completed`, `is_embed`, `is_offline` |
| Engagement | `interaction_count`, `screen_views_count`, `annotation_views_count`, `guide_views_count` |
| Geography | `geo_country_code`, `geo_country_name`, `geo_city` |
| Context | `origin_url`, `demo_owner_name`, `demo_name` |
| Lead Form | `lead_form_exists`, `lead_form_opened`, `lead_form_opened_at`, `lead_form_skipped`, `lead_form_skipped_at`, `lead_form_submitted`, `lead_form_submitted_at` |
| Gates | `email_gate_exists`, `access_gate_exists` |

### Aggregation

Pass one or more dimensions to `group_by` (comma-separated) to receive aggregated counts instead of individual session records. For example, `group_by=date,user_type` returns session counts grouped by both date and user type.

### Response Schema

**Standard response** (individual sessions):

```json
{
  "data": [
    {
      "id": "7b4ab219-ab64-4ae5-8a96-601237c212c1",
      "demo_id": "bacdb032-c3a2-4a38-9b85-a45a5e3639f8",
      "company_id": "3003886f-9618-46b9-9baf-dcbba56a891a",
      "viewer_type": "prospect",
      "user_type": "external",
      "started_at": "2026-03-08T10:37:25.286Z",
      "finished_at": null,
      "is_bounced": false,
      "is_completed": false,
      "interaction_count": 0,
      "screen_views_count": 1,
      "geo_country_code": "USA",
      "geo_country_name": "United States",
      "geo_city": "Dallas",
      "is_embed": true,
      "is_offline": false,
      "origin_url": "<https://app.teamwalnut.com/player/>...",
      "demo_owner_name": "Jane Smith",
      "lead_form_exists": false,
      "lead_form_opened": false,
      "lead_form_opened_at": null,
      "lead_form_skipped": false,
      "lead_form_skipped_at": null,
      "lead_form_submitted": false,
      "lead_form_submitted_at": null,
      "email_gate_exists": false,
      "access_gate_exists": false,
      "annotation_views_count": 1,
      "guide_views_count": 1,
      "demo_name": "Product Overview Demo"
    }
  ],
  "count": 1,
  "total": 253852,
  "limit": 1,
  "offset": 0
}
```

> `count` = rows returned in this response page. `total` = total matching rows across all pages. Use these for pagination logic.

**Single-dimension aggregation** (`group_by=date`):

```json
{
  "data": {
    "2026-03-01": 669,
    "2026-03-02": 822,
    "2026-03-03": 471
  },
  "total": 1962,
  "group_by": ["date"]
}
```

**Multi-dimension aggregation** (`group_by=date,user_type`):

```json
{
  "data": {
    "2026-03-01|external": 669,
    "2026-03-02|external": 820,
    "2026-03-02|internal": 2,
    "2026-03-03|external": 471
  },
  "total": 1962,
  "group_by": ["date", "user_type"]
}
```

> Composite keys use the `|` (pipe) delimiter. Dimensions with zero sessions are **omitted** from the response — absence means zero.

### Sort Order

Results are returned **newest-first** (descending by `started_at`). There is currently no parameter to change sort order.

---

## Field Types, Values & Definitions

Every field returned by `/demo-sessions` is documented below with its data type, possible values, and what it means in the context of Walnut demo analytics.

| Field | Type | Values / Format | Definition |
| --- | --- | --- | --- |
| `id` | UUID | `"7b4ab219-ab64-..."` | Unique session identifier |
| `demo_id` | UUID | `"bacdb032-c3a2-..."` | The demo that was viewed |
| `company_id` | UUID | `"3003886f-9618-..."` | Your Walnut company/account ID |
| `viewer_type` | string | `"prospect"` | `"owner"` |
| `user_type` | string | `"external"` | `"internal"` |
| `started_at` | ISO 8601 | `"2026-03-08T10:37:25.286Z"` | When the session began (always present) |
| `finished_at` | ISO 8601 or null | `"2026-03-08T10:42:00.000Z"` or `null` | When the session ended. `null` means the session is still open or was abandoned without a completion event. |
| `is_bounced` | boolean | `true` / `false` | The viewer exited after the **first screen** with no further interaction. Use bounce rate (`bounced / total`) to measure first-screen effectiveness and gating friction. |
| `is_completed` | boolean | `true` / `false` | The viewer reached the **end of the demo flow** (viewed all screens). Use completion rate (`completed / total`) to measure narrative quality and content stickiness. |
| `is_embed` | boolean | `true` / `false` | `true` = demo was viewed inside an embedded iframe (e.g., on your website). `false` = viewed via direct link or Walnut player. |
| `is_offline` | boolean | `true` / `false` | Whether the session was an offline/downloaded demo view |
| `interaction_count` | integer | 0+ | Total interactive clicks during the session — includes FAB (Floating Action Button) clicks, annotation clicks, and guide step advances. A proxy for engagement depth. |
| `screen_views_count` | integer | 0+ | Number of demo screens viewed in the session. Compare to total screens in the demo to calculate screens viewed (%). |
| `annotation_views_count` | integer | 0+ | Number of **annotation overlays** (tooltips, modals, highlights on screens) the viewer saw. Annotations are the individual visual elements placed on screens to explain features. |
| `guide_views_count` | integer | 0+ | Number of **guide chapters** (not individual steps) viewed. Guides are the structured narrative flows that walk viewers through a demo story. Measured at chapter level, not step level. |
| `geo_country_code` | string | `"USA"`, `"GBR"`, etc. | ISO 3166-1 alpha-3 country code based on viewer IP |
| `geo_country_name` | string | `"United States"` | Full country name |
| `geo_city` | string or null | `"Dallas"` or `null` | City based on IP geolocation. `null` when unavailable. |
| `origin_url` | string | Full URL | The URL where the demo was loaded from (Walnut player URL, or the page where the embed lives) |
| `demo_owner_name` | string | Display name | The Walnut team member who owns/created this demo |
| `demo_name` | string | Demo title | The name of the demo as set in the Walnut builder |
| `lead_form_exists` | boolean | `true` / `false` | Whether this demo has a lead form configured |
| `lead_form_opened` | boolean | `true` / `false` | Whether the lead form was displayed to the viewer |
| `lead_form_opened_at` | ISO 8601 or null | Timestamp or `null` | When the lead form was displayed |
| `lead_form_skipped` | boolean | `true` / `false` | Whether the viewer dismissed the form without submitting |
| `lead_form_skipped_at` | ISO 8601 or null | Timestamp or `null` | When the form was skipped |
| `lead_form_submitted` | boolean | `true` / `false` | Whether the viewer submitted the lead form. Calculate lead form conversion as `submitted / opened`. |
| `lead_form_submitted_at` | ISO 8601 or null | Timestamp or `null` | When the form was submitted |
| `email_gate_exists` | boolean | `true` / `false` | Whether this demo requires an email gate (viewer must enter email before accessing the demo) |
| `access_gate_exists` | boolean | `true` / `false` | Whether this demo has an access gate (password or approval required) |

---

## GET /demo-sessions/summary

Returns a pre-built analytics summary with 7 parallel aggregations, giving you a comprehensive snapshot of your demo performance.

### Summary Includes

1.  **Period** — the date range covered (auto-scopes to last 12 months if no date filters provided)
2.  **Totals** — sessions, completed, bounced
3.  **Averages** — duration (seconds), interactions, screen views
4.  **By user type** — count distribution across user types (`external`, `internal`)
5.  **By viewer type** — count distribution across viewer types (`prospect`, `owner`, `colleague_member`, `colleague_non_member`)
6.  **By embed status** — `embedded` vs. `direct` session counts
7.  **Top 10 demos** — ranked by session count (includes `demo_id`, `demo_name`, `sessions`)
8.  **Top 10 countries** — ranked by session count (includes `country`, `sessions`)

### Optional Filters

The summary endpoint accepts `start_date` and `end_date` query parameters to scope the summary to a specific date range. When omitted, defaults to the last 12 months.

### Response Schema

```json
{
  "period": {
    "start": "2025-03-08",
    "end": "2026-03-08"
  },
  "totals": {
    "sessions": 253852,
    "completed": 253835,
    "bounced": 218118
  },
  "averages": {
    "duration_seconds": 21,
    "interactions": 0.4,
    "screen_views": 1.7
  },
  "by_user_type": {
    "external": 245083,
    "internal": 8769
  },
  "by_viewer_type": {
    "colleague_member": 5298,
    "colleague_non_member": 811,
    "owner": 2660,
    "prospect": 245083
  },
  "by_embed": {
    "direct": 24589,
    "embedded": 229263
  },
  "top_demos": [
    {
      "demo_id": "4c1e1a49-ba4d-4951-9f6a-6005648e8eea",
      "demo_name": "Product Overview Demo",
      "sessions": 31668
    }
  ],
  "top_countries": [
    {
      "country": "United States",
      "sessions": 107132
    }
  ]
}
```

> Note: `averages.duration_seconds` is an integer. `averages.interactions` and `averages.screen_views` are floats.

---

# Examples

All examples use the production base URL. Replace `YOUR_API_KEY` with the key provided by your Walnut account team.

## Health Check (no authentication required)

```bash
curl <https://customer-api.teamwalnut.com/health>
```

**Response:**

```json
{ "status": "ok" }
```

## Retrieve Last 10 Sessions

```bash
curl -H "x-api-key: YOUR_API_KEY" \\
  "<https://customer-api.teamwalnut.com/demo-sessions?limit=10>"
```

## Filter by Date Range and User Type

```bash
curl -H "x-api-key: YOUR_API_KEY" \\
  "<https://customer-api.teamwalnut.com/demo-sessions?start_date=2025-01-01&end_date=2025-12-31&user_type=external&limit=50>"
```

## Select Specific Fields

```bash
curl -H "x-api-key: YOUR_API_KEY" \\
  "<https://customer-api.teamwalnut.com/demo-sessions?fields=id,demo_id,started_at,is_completed,user_type&limit=20>"
```

## Aggregate by Date

```bash
curl -H "x-api-key: YOUR_API_KEY" \\
  "<https://customer-api.teamwalnut.com/demo-sessions?start_date=2025-01-01&group_by=date>"
```

## Multi-Dimensional Aggregation

```bash
curl -H "x-api-key: YOUR_API_KEY" \\
  "<https://customer-api.teamwalnut.com/demo-sessions?group_by=date,user_type>"
```

## Analytics Summary

```bash
curl -H "x-api-key: YOUR_API_KEY" \\
  "<https://customer-api.teamwalnut.com/demo-sessions/summary>"
```

## Summary with Date Filter

```bash
curl -H "x-api-key: YOUR_API_KEY" \\
  "<https://customer-api.teamwalnut.com/demo-sessions/summary?start_date=2025-01-01>"
```

## Filter by Specific Demo

```bash
curl -H "x-api-key: YOUR_API_KEY" \\
  "<https://customer-api.teamwalnut.com/demo-sessions?demo_id=YOUR_DEMO_UUID&limit=100>"
```

---

# Pagination

The `/demo-sessions` endpoint returns up to `limit` rows per request (default 1,000; maximum 10,000). Use the `offset` parameter to paginate through larger datasets.

## Example: Paginating Through Results

```
# Page 1 (rows 0-999)
curl -H "x-api-key: YOUR_API_KEY" \\
  "<https://customer-api.teamwalnut.com/demo-sessions?limit=1000&offset=0>"

# Page 2 (rows 1000-1999)
curl -H "x-api-key: YOUR_API_KEY" \\
  "<https://customer-api.teamwalnut.com/demo-sessions?limit=1000&offset=1000>"

# Page 3 (rows 2000-2999)
curl -H "x-api-key: YOUR_API_KEY" \\
  "<https://customer-api.teamwalnut.com/demo-sessions?limit=1000&offset=2000>"
```

Continue incrementing `offset` by your `limit` value until fewer rows than `limit` are returned, indicating you have reached the end of the dataset.

---

# Error Handling

The API uses standard HTTP status codes. Error responses return a JSON body with a descriptive message.

| Status Code | Meaning | Common Cause |
| --- | --- | --- |
| 200 | Success | Request completed successfully |
| 400 | Bad Request | Invalid date format, unknown field name, or malformed parameter |
| 401 | Unauthorized | Missing or invalid API key |
| 429 | Too Many Requests | Rate limit exceeded; retry with exponential backoff |
| 500 | Internal Server Error | Unexpected server issue; contact support if persistent |

**Error response body:**

```json
{ "error": "Invalid fields requested" }
```

> ℹ️ Error responses never expose internal details. If you receive a 500 error consistently, contact your Walnut account team with the request URL and timestamp.

---

# What Can You Do With This Data?

The fields returned by the API map directly to actionable metrics. Here are recipes your team can implement immediately.

## Key Metrics You Can Calculate

| Metric | Formula | Why It Matters |
| --- | --- | --- |
| **Bounce Rate** | `is_bounced=true` count / total sessions | Measures first-screen effectiveness. High bounce = weak opener or too-early gating. |
| **Completion Rate** | `is_completed=true` count / total sessions | Measures narrative quality. Higher completion = story resonates and holds attention. |
| **Lead Form Conversion** | `lead_form_submitted` / `lead_form_opened` | Is your form friction too high? Low conversion suggests the form appears too early or asks too much. |
| **Engagement Depth** | `interaction_count` • `screen_views_count` per session | Are prospects exploring or skimming? Higher depth = stronger buying intent. |
| **Internal vs External Split** | Filter by `user_type` | High internal usage may mean reps are previewing, not sending to prospects. Track the ratio over time. |
| **Embed Performance** | Compare `is_embed=true` vs `false` sessions | Are website embeds converting better than direct links? Optimize placement accordingly. |
| **Identification Coverage** | Sessions with `email_gate_exists=true` or `lead_form_exists=true` / total | The more viewers you can identify, the more complete your attribution picture becomes. Aim for 70–80% coverage. |
| **Top Demos by Engagement** | Sort by avg `interaction_count`, not just session count | Volume ≠ quality. A demo with fewer sessions but high interaction may be your best closer. |

## Common Integration Patterns

-   **CRM sync:** Pull sessions daily, match `origin_url` or lead form data to opportunities in Salesforce/HubSpot. Use `demo_name` and `demo_owner_name` for attribution.
-   **BI dashboards:** Use `group_by=date` for trend charts, `/demo-sessions/summary` for executive snapshots. The `period` object tells you the exact date range covered.
-   **Hot lead alerts:** Poll for sessions where `is_completed=true` AND `interaction_count > 5` to notify sales of high-intent viewers.
-   **Re-engagement triggers:** Flag open deals where recent sessions show `is_bounced=true` — send a shorter, more targeted demo or playlist.
-   **Embed optimization:** Compare `by_embed` data in the summary to decide whether to invest in more website embeds or direct-link campaigns.
-   **Pagination loop:** Increment `offset` by your `limit` value until `count \< limit`, indicating you’ve reached the end of the dataset.

---

# Current Limitations

This is the initial release of the Customer Data API. Please note the following current constraints:

-   **Read-only access:** the API supports data retrieval only; there are no write endpoints.
-   **Maximum of 10,000 rows per request.** Use pagination for larger datasets.
-   **Offset-based pagination only.** There is no cursor-based pagination. For very large datasets, offset pagination may slow down at high offsets.
-   **API key changes** (including revocations) may take up to 5 minutes to take effect due to authorization caching.
-   **No self-service key rotation.** If you lose your API key, contact your Walnut account representative for a new one.
-   **Demo session data only.** Additional data types (demo metadata, playlist sessions, admin analytics) will be added in future releases.
-   **Fixed sort order.** Results are always returned newest-first by `started_at`. Custom sorting is not yet supported.
-   **Summary auto-scopes to 12 months.** When no `start_date`/`end_date` is provided, `/demo-sessions/summary` defaults to the last 12 months.

---

# Support

If you need assistance with the Customer Data API, your Walnut account team is here to help.

-   **API key requests:** Contact your Walnut account representative to provision a new key or revoke an existing one.
-   **Technical issues:** Include the request URL, timestamp, and HTTP status code when reporting problems.
-   **Feature requests:** Let your account team know what additional data or capabilities would be valuable for your use case.
