# Supersonik Client API & Setup Guide

**API Host:** `https://api.supersonik.ai`
**App Host:** `https://app.supersonik.ai`
**Version:** v1
**Last Updated:** 2026-08-03

---

## Contents

1. [Setting up a demo](#1-setting-up-a-demo)
2. [Query parameters](#2-query-parameters)
3. [Authentication](#3-authentication)
4. [Endpoints](#4-endpoints)
5. [Shared error responses](#5-shared-error-responses)
6. [Data returned by the API](#6-data-returned-by-the-api)
7. [Rate limits and support](#7-rate-limits-and-support)

---

## 1. Setting up a demo

There are three ways to put a Supersonik demo in front of a prospect. They are not
mutually exclusive. You can use all three against the same demo.

| Option | URL shape | Use when |
|--------|-----------|----------|
| **Plain link** | `https://app.supersonik.ai/d/<demo-slug>` | Emails, cadences, chat, anywhere you just need a link |
| **Embedded iframe** | `https://app.supersonik.ai/d/<demo-slug>/embed` | The demo should run inside one of your own pages |
| **Contextualized link** | `https://app.supersonik.ai/d/c/<token>` | You want the agent to know who the prospect is before they arrive |

### Option A: Plain link

The simplest setup. Every published demo has a slug, and the full link is:

```
https://app.supersonik.ai/d/<demo-slug>
```

Retrieve your slugs and their full URLs from [`GET /v1/client/launch-configs`](#41-list-published-demos).
The `url` field on each item is the ready-to-use link.

```
https://app.supersonik.ai/d/enterprise-demo-en
```

No integration work is required. Send the link and the prospect gets the full demo
experience on our domain.

### Option B: Embedded iframe

Runs the demo inside a page you control. Use the `/embed` variant of the demo path.

Replace `YOUR-DEMO-SLUG` with your slug from
[`GET /v1/client/launch-configs`](#41-list-published-demos):

```html
<div style="width:100%; height:600px;">
  <iframe
    src="https://app.supersonik.ai/d/YOUR-DEMO-SLUG/embed?native=true"
    style="width:100%; height:100%; border:none;"
    allow="camera; microphone; fullscreen"
    allowfullscreen
  ></iframe>
</div>
```

This is the same snippet the Supersonik app gives you from the share menu of any
published demo, so you can copy it from there instead of from this document.

**Required for the embed to work:**

- **`allow="microphone"`**: the demo is a voice conversation. Without microphone
  permission delegated to the iframe, the prospect cannot speak to the agent.
- **`allow="camera"`**: only needed if your demo is configured to show a camera
  preview before the call. If it is not, use `allow="microphone; fullscreen"`.
- **`allowfullscreen`**: lets the prospect expand the demo, which materially
  improves the experience on smaller viewports.
- **HTTPS**: browsers only grant microphone access in a secure context.

**Sizing.** The wrapper `<div>` is not decoration: it is what makes the embed work on a
page that does not set a height of its own. A percentage `height` on an iframe resolves
against its parent, so an iframe whose parent has an automatic height has nothing to
resolve against and collapses to its intrinsic 150px. The demo then lays itself out
inside a 150px viewport, which reads as an almost-empty strip rather than as a short
frame. Stating the height on the wrapper avoids that everywhere.

To adjust it:

- **Taller or shorter:** change `height:600px` on the wrapper. Give it a generous
  height; a cramped frame is a worse demo.
- **Fill the viewport:** use `height:100dvh` instead. A full-viewport container, or a
  full-screen overlay opened from a call to action, gives the best experience.
- **Leave the width fluid.** The demo adapts to the width of the box you give it, not to
  the device, so it uses your section's own width. It opens up its layout from roughly
  968px wide, and again from 1200px, where the agent, the transcript and the shared
  screen each get room. Capping the wrapper at a few hundred pixels pins the demo to its
  most compact layout on every screen, including a large desktop monitor, and letterboxes
  the shared screen inside a narrow portrait box.

**Domains.** The embed page can be framed from any domain. There is no allowlist to
configure on our side.

**Your page's `Permissions-Policy`.** If the hosting page sends a
`Permissions-Policy` header that does not permit `microphone`, the browser will block
the microphone even though the iframe requests it. Make sure microphone is permitted
on the page that hosts the embed.

### Option C: Contextualized link

Use this when you want the agent to already know something about the prospect: their
name, company, role, what they were reading, notes from a previous call. You send us
the context, we return a unique link.

Call [`POST /v1/client/demos`](#42-create-a-contextualized-demo-link) with the demo
slug and a `context` object, then send the returned `url` to your prospect:

```
https://app.supersonik.ai/d/c/xK9mPq2wLn4r
```

Contextualized links are reusable, do not expire while the underlying demo stays
published, and carry immutable context. To change the context, create a new link.

Contextualized links are for sharing directly, not for embedding. They have no
`/embed` variant. To personalize a demo inside your own page, use the embed URL with
[query parameters](#2-query-parameters).

---

## 2. Query parameters

You can append query parameters to a plain link, an embed URL, or a contextualized
link. They serve two purposes:

**Attribution.** Tag each placement so you can tell which campaign, email, or page
drove a session:

```
https://app.supersonik.ai/d/enterprise-demo-en?utm_medium=email&utm_campaign=spring-spotlight
https://app.supersonik.ai/d/enterprise-demo-en?utm_medium=email&utm_persona=1
```

**Personalization.** Pass what you already know so the prospect does not have to
type it. Passing `email` is the most common case, and it can remove a form step:

```
https://app.supersonik.ai/d/enterprise-demo-en?email=jane.smith@acme.com
```

**How they come back to you.** Every parameter on the URL is recorded against the
demo and returned in the `params` object of
[`GET /v1/client/demos/{demo_id}`](#44-get-demo-details), merged with any values the
prospect entered in the pre-demo form. Parameter names are yours to choose. Use
whatever labels fit your reporting.

**Query parameters vs. contextualized links.** Query parameters are the right tool
for a handful of flat, non-sensitive values, and they are visible in the URL. A
contextualized link is the right tool for richer or more sensitive data: the payload
travels server-to-server and only an opaque token appears in the URL.

---

## 3. Authentication

All API requests require a Bearer token.

```
Authorization: Bearer <API_KEY>
```

The API key is issued by the Supersonik team. It:

- Authenticates your requests
- Identifies your organization
- Scopes every response to your organization's data only

Data belonging to other organizations is never visible through this API.

**Keep your API key secure.** It is a server-side credential. Do not expose it in
client-side code, in a browser, or in a public repository.

---

## 4. Endpoints

| Method | Path | Purpose |
|--------|------|---------|
| `GET` | `/v1/client/launch-configs` | List your published demos and their links |
| `POST` | `/v1/client/demos` | Create a contextualized demo link |
| `GET` | `/v1/client/demos` | List demos that have run |
| `GET` | `/v1/client/demos/{demo_id}` | Full detail for one demo |
| `GET` | `/v1/client/demos/{demo_id}/transcript` | Conversation transcript for one demo |

---

### 4.1 List published demos

```
GET /v1/client/launch-configs
```

Returns every published demo configuration for your organization, with a ready-to-use
link for each. This endpoint takes no query parameters and returns the full set in one
response.

**Response: 200 OK**

```json
{
  "items": [
    {
      "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "name": "Product Demo - Enterprise",
      "description": "Interactive demo showcasing enterprise features",
      "url": "https://app.supersonik.ai/d/enterprise-demo-en"
    },
    {
      "id": "7cb92a31-8824-4a91-bc12-1d847e22ab91",
      "name": "Product Demo - Starter",
      "description": "Quick tour of starter plan capabilities",
      "url": "https://app.supersonik.ai/d/starter-demo-en"
    }
  ],
  "pagination": {
    "total_count": 2,
    "max_page": 1
  }
}
```

| Field | Type | Description |
|-------|------|-------------|
| `items[].id` | string (UUID) | Identifier of the demo configuration |
| `items[].name` | string | Display name |
| `items[].description` | string or null | Optional description |
| `items[].url` | string | Full demo link, ready to send |
| `pagination.total_count` | integer | Number of published demos |
| `pagination.max_page` | integer | Always `1`; this endpoint is not paginated |

The slug in `url` is the `<demo-slug>` used by the embed path and by
`demo_url_slug` when creating a contextualized link.

---

### 4.2 Create a contextualized demo link

```
POST /v1/client/demos
```

Generates a unique demo URL with your context data attached. When the prospect opens
it, the agent runs the demo with that context available.

**Headers**

| Header | Required | Value |
|--------|----------|-------|
| `Authorization` | Yes | `Bearer <API_KEY>` |
| `Content-Type` | Yes | `application/json` |

**Request body**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `demo_url_slug` | string | Yes | Slug of the demo to run. Must be a published demo belonging to your organization. |
| `context` | object | Yes | Any JSON object. No fixed schema. |

```json
{
  "demo_url_slug": "enterprise-demo-en",
  "context": {
    "prospect_name": "Jane Smith",
    "company": "Acme Corp",
    "role": "VP of Engineering",
    "interests": ["CI/CD pipelines", "security scanning"],
    "account_tier": "enterprise",
    "meeting_notes": "Interested in migrating from Jenkins. Team of 50 developers."
  }
}
```

**Context limits.** The `context` object must not exceed **32 KB** serialized, and must
not nest more than **5 levels** deep. Requests that exceed either limit are rejected
with `400 Bad Request`.

**Response: 201 Created**

```json
{
  "id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
  "token": "xK9mPq2wLn4r",
  "url": "https://app.supersonik.ai/d/c/xK9mPq2wLn4r"
}
```

| Field | Type | Description |
|-------|------|-------------|
| `id` | string (UUID) | Identifier of the created link |
| `token` | string | Opaque token embedded in the URL |
| `url` | string | The link to send to your prospect |

**Link behaviour**

- **Reusable**: can be opened multiple times; each visit starts a new session with the same context.
- **No expiry**: stays valid while the underlying demo remains published.
- **Immutable context**: create a new link to change the context.

**Errors**

`400 Bad Request`: the body is invalid, the context exceeds the size or depth limit,
or `demo_url_slug` does not match a published demo for your organization.

```json
{
  "error": "BadRequest",
  "detail": "No published demo found for demo_url_slug 'invalid-slug'"
}
```

---

### 4.3 List demos

```
GET /v1/client/demos
```

Returns a paginated, newest-first list of demos that have run for your organization.
Use this to sync demo activity into your CRM or warehouse, detect newly completed
demos, or backfill history.

**Query parameters**

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `page` | integer | No | Page number. Defaults to `1`. Must be greater than `0`. |
| `limit` | integer | No | Results per page. Must be greater than `0` and within the maximum configured for your integration. |
| `start_time_from` | string (ISO 8601) | No | Inclusive lower bound on `start_time`. |
| `start_time_to` | string (ISO 8601) | No | Inclusive upper bound on `start_time`. |
| `demo_type` | `prod` \| `test` | No | Which demo types to return. Repeat the parameter to select several. Defaults to `prod` only. |
| `validity` | `valid` \| `invalid` \| `unknown` | No | Which validity verdicts to return. Repeat the parameter to select several. Defaults to `valid` and `unknown`, i.e. everything we have not classified as invalid. |

Results are ordered newest-first by `start_time`, then by demo ID descending so that
ordering is stable when timestamps tie. Both date filters are inclusive.

**Default filtering.** With no `demo_type` or `validity` parameters you get production
demos that we have not classified as invalid. Internal test runs and demos where nobody
engaged are excluded, which is what a CRM or warehouse sync usually wants. Pass the
parameters explicitly to widen the selection:

```
GET /v1/client/demos                                        # prod, valid + unknown
GET /v1/client/demos?demo_type=prod&demo_type=test          # include internal test runs
GET /v1/client/demos?validity=valid&validity=invalid&validity=unknown   # every verdict
GET /v1/client/demos?validity=unknown                       # only demos with no verdict
```

`unknown` is part of the default on purpose: `is_valid` is written when post-call
processing finishes, so a demo that is still running, or still being processed, has no
verdict yet. Excluding `unknown` would hide those demos until processing completes, and
a sync that advances `start_time_from` past them in the meantime would never see them.

**Response: 200 OK**

```json
{
  "items": [
    {
      "id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
      "type": "prod",
      "start_time": "2026-05-04T09:30:02.000000Z",
      "end_time": "2026-05-04T09:41:14.000000Z",
      "duration_seconds": 672,
      "participant_joined": true,
      "is_valid": true,
      "agent": {
        "id": "b2c3d4e5-6789-01bc-def0-234567890abc",
        "name": "Enterprise Sales Agent"
      },
      "launch_config": {
        "id": "c3d4e5f6-7890-12cd-ef01-34567890abcd",
        "name": "Enterprise Demo - EN"
      },
      "details_url": "https://api.supersonik.ai/v1/client/demos/a1b2c3d4-5678-90ab-cdef-1234567890ab"
    }
  ],
  "pagination": {
    "total_count": 42,
    "max_page": 5
  }
}
```

| Field | Type | Description |
|-------|------|-------------|
| `id` | string (UUID) | Demo identifier |
| `type` | string | `prod` for prospect-run demos, `test` for internal test runs |
| `start_time` | string (ISO 8601) or null | When the demo started |
| `end_time` | string (ISO 8601) or null | When it ended. `null` while in progress. |
| `duration_seconds` | integer or null | Derived from start and end. `null` while in progress. |
| `participant_joined` | boolean | Whether anyone actually joined the call |
| `is_valid` | boolean or null | Whether we classified the session as valid. `null` if not yet evaluated. |
| `agent` | object | `{ "id": UUID, "name": string }` |
| `launch_config` | object or null | `{ "id": UUID, "name": string }`, matching an entry from `GET /launch-configs` |
| `details_url` | string | Link to the full detail resource for this demo |
| `pagination.total_count` | integer | Total demos matching the query across all pages |
| `pagination.max_page` | integer | Last available page for the current `limit` |

**Incremental sync.** Store the latest `start_time` you have processed and use it as
the next `start_time_from`. Because the filter is inclusive, de-duplicate by demo `id`
when polling overlapping windows.

**Errors**

`400 Bad Request`: `start_time_from` is after `start_time_to`.

```json
{
  "error": "BadRequest",
  "detail": "start_time_from must not be after start_time_to"
}
```

---

### 4.4 Get demo details

```
GET /v1/client/demos/{demo_id}
```

Full detail for a single demo: everything from the list view, plus the URL parameters
and context it ran with, who joined, the insights we derived, and a link to the
transcript.

**Response: 200 OK**

```json
{
  "id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
  "type": "prod",
  "start_time": "2026-05-04T09:30:02.000000Z",
  "end_time": "2026-05-04T09:41:14.000000Z",
  "duration_seconds": 672,
  "participant_joined": true,
  "is_valid": true,
  "agent": {
    "id": "b2c3d4e5-6789-01bc-def0-234567890abc",
    "name": "Enterprise Sales Agent"
  },
  "agent_config_version": 8,
  "launch_config": {
    "id": "c3d4e5f6-7890-12cd-ef01-34567890abcd",
    "name": "Enterprise Demo - EN"
  },
  "params": {
    "email": "jane.smith@acme.com",
    "utm_medium": "email",
    "utm_campaign": "spring-spotlight"
  },
  "demo_context": {
    "prospect_name": "Jane Smith",
    "company": "Acme Corp"
  },
  "participants": [
    {
      "display_name": "Jane Smith",
      "joined_at": "2026-05-04T09:30:11.000000Z",
      "left_at": "2026-05-04T09:41:09.000000Z"
    }
  ],
  "insights": [
    {
      "slug": "interest_level",
      "name": "Interest level",
      "output_type": "string",
      "value": "high"
    },
    {
      "slug": "requested_trial",
      "name": "Requested a trial",
      "output_type": "boolean",
      "value": true
    }
  ],
  "transcript_url": "https://api.supersonik.ai/v1/client/demos/a1b2c3d4-5678-90ab-cdef-1234567890ab/transcript"
}
```

| Field | Type | Description |
|-------|------|-------------|
| `agent_config_version` | integer or **null** | Version of the agent configuration used. See the note below. |
| `params` | object or null | Query parameters from the demo URL, merged with values the prospect entered in the pre-demo form. All values are strings. |
| `demo_context` | object or null | The `context` object supplied when the contextualized link was created. `null` for plain and embedded links. |
| `participants[].display_name` | string | Display name of the participant |
| `participants[].joined_at` | string (ISO 8601) | When they joined |
| `participants[].left_at` | string (ISO 8601) or null | When they left. `null` if still connected. |
| `insights[].slug` | string | Stable machine-readable key for the insight |
| `insights[].name` | string | Human-readable label |
| `insights[].output_type` | string | Value type of this insight |
| `insights[].value` | number, string, boolean, array of strings, or null | The derived value. `null` when not determined. |
| `transcript_url` | string | Link to the transcript resource. Requires the same API key. |

> **`agent_config_version` can be `null`, permanently.** Demos pinned to a configuration
> that predates version numbering have no version to report and never will. This is not
> a gap that gets backfilled. Treat the field as nullable in your pipeline.

Fields shared with the list response (`id`, `type`, `start_time`, `end_time`,
`duration_seconds`, `participant_joined`, `is_valid`, `agent`, `launch_config`) carry
the same meaning as documented in [4.3](#43-list-demos).

---

### 4.5 Get demo transcript

```
GET /v1/client/demos/{demo_id}/transcript
```

The full conversation for one demo, in order.

**Response: 200 OK**

```json
{
  "demo_id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
  "messages": [
    {
      "role": "Enterprise Sales Agent",
      "content": "Hi Jane, thanks for joining. Want me to start with the pipeline view?",
      "timestamp": "2026-05-04T09:30:14.000000Z",
      "source": "voice"
    },
    {
      "role": "Participant",
      "content": "Yes please, and show me the security scanning after.",
      "timestamp": "2026-05-04T09:30:21.000000Z",
      "source": "voice"
    },
    {
      "role": "Participant",
      "content": "can you share pricing?",
      "timestamp": "2026-05-04T09:36:02.000000Z",
      "source": "chat"
    }
  ]
}
```

| Field | Type | Description |
|-------|------|-------------|
| `demo_id` | string (UUID) | Demo this transcript belongs to |
| `messages[].role` | string | Who spoke. Agent turns carry the agent's configured display name; prospect turns carry the literal `Participant`. See the note below before branching on this value. |
| `messages[].content` | string | What was said or typed |
| `messages[].timestamp` | string or null | When the message occurred |
| `messages[].source` | string or null | `voice` if spoken, `chat` if typed into the chat panel |

> **`role` is a display label, not a stable enum.** Agent messages carry the agent's
> configured display name, which is the same value returned as `agent.name` by
> [`GET /demos`](#43-list-demos) and [`GET /demos/{demo_id}`](#44-get-demo-details), and
> which changes if the agent is renamed. Prospect messages carry the literal string
> `Participant`.
>
> To classify messages reliably, either test for `role == "Participant"` to identify
> prospect turns, or fetch `agent.name` from the demo details and compare against it.
> Do not hardcode the agent name, and do not expect OpenAI-style `assistant` / `user`
> values.

---

## 5. Shared error responses

All endpoints can return the following.

**401 Unauthorized**: the API key is missing, invalid, or expired.

```json
{
  "error": "NotAuthenticated",
  "detail": "Invalid or missing authentication token"
}
```

Depending on which layer rejects the request, this may instead arrive as
`{"detail": "Missing authentication credentials"}` or `{"detail": "Invalid Bearer token"}`.
Treat any `401` the same way.

**404 Not Found**: the requested `demo_id` does not exist, or does not belong to your
organization.

**422 Unprocessable Entity**: a query parameter or path parameter failed validation,
for example a malformed datetime, `page=0`, or a `demo_id` that is not a valid UUID.

```json
{
  "detail": [
    {
      "type": "greater_than",
      "loc": ["query", "page"],
      "msg": "Input should be greater than 0",
      "input": "0",
      "ctx": {"gt": 0}
    }
  ]
}
```

**500 Internal Server Error**: an unexpected server error.

```json
{
  "error": "InternalServerError",
  "detail": "An unexpected error occurred"
}
```

---

## 6. Data returned by the API

Some responses contain personal data about your prospects. Handle them according to
your own data policies.

| Where | What it can contain |
|-------|---------------------|
| `params` on demo details | Whatever you put in the demo URL, commonly including `email` |
| `demo_context` on demo details | Whatever you sent when creating a contextualized link |
| `participants[].display_name` on demo details | The prospect's display name |
| `insights` on demo details | Values derived from the conversation |
| `messages[].content` on the transcript | The full verbatim conversation, spoken and typed |

The list endpoints (`GET /launch-configs`, `GET /demos`) do not return `params`,
`demo_context`, or transcript content. If you only need activity volumes and timings,
the list endpoint is sufficient and avoids pulling personal data.

---

## 7. Rate limits and support

**Rate limits.** Contact the Supersonik team for the limits applicable to your
integration.

**Support.** For questions or issues, contact the Supersonik team.
