API Host: https://api.supersonik.ai
App Host: https://app.supersonik.ai
Version: v1
Last Updated: 2026-08-03
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.
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:
<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, useallow="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:600pxon the wrapper. Give it a generous height; a cramped frame is a worse demo. - Fill the viewport: use
height:100dvhinstead. 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 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
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}, 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
{
"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. |
{
"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
{
"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.
{
"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
{
"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.
{
"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
{
"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_versioncan benull, 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.
4.5 Get demo transcript
GET /v1/client/demos/{demo_id}/transcript
The full conversation for one demo, in order.
Response: 200 OK
{
"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 |
roleis a display label, not a stable enum. Agent messages carry the agent's configured display name, which is the same value returned asagent.namebyGET /demosandGET /demos/{demo_id}, and which changes if the agent is renamed. Prospect messages carry the literal stringParticipant.To classify messages reliably, either test for
role == "Participant"to identify prospect turns, or fetchagent.namefrom the demo details and compare against it. Do not hardcode the agent name, and do not expect OpenAI-styleassistant/uservalues.
5. Shared error responses
All endpoints can return the following.
401 Unauthorized: the API key is missing, invalid, or expired.
{
"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.
{
"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.
{
"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.