Technical guide for embedding Tellet interviews in quantitative survey platforms (Dimensions, Decipher, Forsta) using iframes.
Embedding places Tellet interviews inside quantitative surveys as iframe components, so participants complete AI-powered qualitative interviews within their survey session.
Use for:
Don't use for:
Survey ā Tellet (URL parameters):
Tellet ā Survey (PostMessage API):
See PostMessage API section below for complete details on all message types.
From Tellet share URL https://chat.tellet.ai/507f1f77bcf86cd799439011, the project ID is the 24-character string at the end: 507f1f77bcf86cd799439011
In survey's HTML editor:
<iframe
id="telletFrame1"
src="https://chat.tellet.ai/YOUR_PROJECT_ID?environment=sandbox&language=English&skipConsent=true&participant_id=TEST123"
title="Tellet Interview"
style="width: 100%; height: 600px; border: none;"
allow="camera; microphone; autoplay;"
></iframe>
Replace YOUR_PROJECT_ID with actual 24-character ID.
Verify:
Use environment=sandbox for testing to prevent test data in results.
<iframe
id="telletFrame1"
src="https://chat.tellet.ai/PROJECT_ID?environment=sandbox&store=Tesco&date=2024-02-01&score=3&language=English&skipConsent=true&participant_id={{RESPONDENT_ID}}"
title="Tellet Interview"
style="width: 100%; height: 600px; border: none;"
allow="camera; microphone; autoplay;"
></iframe>
{{ā¦}} here is a generic stand-in ā replace it with your survey platform's own variable syntax (see Platform Variable Syntax below).
Key elements:
id="telletFrame1" - Unique ID (increment for multiple iframes)src="..." - Tellet project URL with parametersstyle="..." - Size and appearanceallow="..." - Permissions for camera/microphoneAdd once per survey page (not per iframe):
<script>
window.addEventListener('message', (event) => {
let frameId = null;
const iframes = window.document.getElementsByTagName('iframe');
// Find which iframe sent the message
for (let i = 0; i < iframes.length; i++) {
if (event.source === iframes[i].contentWindow) {
frameId = iframes[i].id;
break;
}
}
if (!frameId) return; // Message from non-Tellet source
// Handle different message types
if (event.data.type === 'INPUT') {
// Store data from Tellet on the iframe
for (const [attr, value] of Object.entries(event.data.data)) {
document.getElementById(frameId).setAttribute(attr, value);
}
}
if (event.data.type === 'CONVERSATION_COMPLETE') {
// Full interview transcript - persist it BEFORE the survey advances.
// This always arrives before END, so the data is safely stored by the
// time your survey continues.
document
.getElementById(frameId)
.setAttribute('conversation_complete_payload', JSON.stringify(event.data.data));
}
if (event.data.type === 'END') {
// Interview completed and transcript delivered - continue survey
const questionNumber = frameId.replace('telletFrame', '');
document.getElementById('q' + questionNumber).click();
}
if (event.data.type === 'OUTPUT') {
// Return stored iframe attributes to Tellet
const attributes = {};
Array.from(document.getElementById(frameId).attributes).forEach((attr) => {
attributes[attr.name] = attr.value;
});
document.getElementById(frameId).contentWindow.postMessage(attributes, '*');
}
});
</script>
Script functions:
Hidden checkbox/button that script clicks to continue:
<input type="checkbox" id="q1" name="q1" value="completed" style="display:none;" />
ID pattern: q + iframe number
telletFrame1 ā q1telletFrame2 ā q2participant_id ā Purpose: Unique respondent identifier; Example: participant_id={{RESP_ID}}; Required: Yeslanguage ā Purpose: Interview language; Example: language=Spanish; Required: RecommendedskipConsent ā Purpose: Skip consent screen; Example: skipConsent=true; Required: Recommendedsource ā Purpose: Survey platform identifier; Example: source=dimensions; Required: Optionalenvironment ā Purpose: Test mode (responses kept separate from real results); Example: environment=sandbox; Required: Testing onlyinputMethods ā Purpose: Override allowed response input methods (comma-separated); Example: inputMethods=text,audio; Required: OptionalinputMethods is a chat configuration parameter ā like language and skipConsent, it shapes how the interview is presented to the participant. When set, it overrides the input methods configured on the project (text / voice / video / image) for this specific session. This affects:
Parameter name note: The URL parameter is
inputMethods(plural, with trailings). The per-message fieldinputMethod(singular) returned in theCONVERSATION_COMPLETEpayload is a different thing ā see the CONVERSATION_COMPLETE section below. UsinginputMethod(singular) as a URL parameter has no effect.
Accepted values: text, audio, video, image. The alias voice is accepted and mapped to audio. Values are case-insensitive. Invalid values are silently dropped; if none remain the parameter is ignored and the project's configured input methods are used.
Examples:
inputMethods=text ā text-only responsesinputMethods=text,audio ā text and voiceinputMethods=voice ā same as inputMethods=audioNote: Unlike source and language, inputMethods is applied client-side to configure the chat session ā it is not stored as a structured metadata field on the conversation record. Use source to tag the conversation in the dashboard.
Pass survey data to personalise interview:
score ā Purpose: Prior rating/score; Example: score=3segment ā Purpose: Customer segment; Example: segment=premiumproduct ā Purpose: Product name; Example: product=iPhone%2015store ā Purpose: Location; Example: store=Tescodate ā Purpose: Reference date; Example: date=2025-02-03[%VARIABLE%]; Example: participant_id=[%ResponseID%]<#VARIABLE#>; Example: participant_id=<#RUID#><%VARIABLE%>; Example: participant_id=<%RSP_ID%>%20; Example Original: iPhone 15; Example Encoded: iPhone%2015%28 %29; Example Original: (256GB); Example Encoded: %28256GB%29%2B; Example Original: C++; Example Encoded: C%2B%2B%26; Example Original: A&B; Example Encoded: A%26BInsert placeholders in Tellet questions that get replaced with URL parameter values.
Example:
"Thank you for rating us %%score%%. Can you tell me more about your experience at %%store%%?"?score=3&store=Tesco"Thank you for rating us 3. Can you tell me more about your experience at Tesco?""You gave us a %%score%%. What was the main reason?"; URL Parameters: ?score=3; Result: "You gave us a 3. What was the main reason?""How would you improve the %%product%%?"; URL Parameters: ?product=iPhone%2015; Result: "How would you improve the iPhone 15?""Thinking about %%store%% on %%date%%, what stood out?"; URL Parameters: ?store=Tesco&date=2024-02-15; Result: "Thinking about Tesco on 2024-02-15, what stood out?"?score={{Q1}}&store={{Q5}}%%parameterName%% in Tellet questions: "You rated %%product%% a %%score%%. Why?"Fallback: If a variable isn't provided, Tellet strips the %% markers and leaves the bare placeholder word in the question text (e.g., %%store%% becomes store). Always provide all referenced variables so participants never see a leftover placeholder word.
Tellet uses the browser's PostMessage API to communicate between the embedded iframe and the parent survey page. Tellet supports both legacy and enhanced message types for maximum compatibility.
The following message types provide rich, real-time data for survey integrations:
Purpose: Real-time updates as the conversation progresses through different states.
When Fired:
Data Structure:
{
"type": "STATUS_CHANGE",
"data": {
"status": "in_progress", // Current conversation status
"conversationId": "67c84d28...", // Unique conversation ID
"timestamp": "2025-01-09T12:45:30.123Z", // ISO 8601 timestamp
"progress": {
"count": 3, // Questions answered
"limit": 10, // Total questions
"done": false // Completion status
}
}
}
Possible Status Values:
prescreening - Screening questions in progressprescreening_failed - Did not pass screeninginitiated - Conversation createdin_progress - Participant answering questionsdone - All questions answeredanalyzing - AI digest generation in progressdigested - AI analysis completeabandoned - Participant left without completingExample Usage:
window.addEventListener('message', (event) => {
if (event.data.type === 'STATUS_CHANGE') {
const { status, progress } = event.data.data;
// Update progress bar in survey
updateProgressBar(progress.count, progress.limit);
// Log status for analytics
console.log(`Interview ${status}: ${progress.count}/${progress.limit}`);
}
});
Purpose: Provides complete conversation data when the interview finishes, including all messages and metadata.
When Fired: Immediately after the interview completes. It always fires BEFORE the legacy END message, so the full transcript is in your page's hands before anything reacts to END (such as advancing the survey).
Data Structure:
{
"type": "CONVERSATION_COMPLETE",
"data": {
"conversationId": "67c84d28...",
"status": "done",
"metadata": {
"participant_id": "ABC123",
"language": "English",
"source": "yourPanel",
// ... other custom parameters
// Note: Sensitive fields (email, password, token, secret, key) are automatically sanitized
},
"messages": [
{
"role": "assistant",
"text": "Hello! How can I help?",
"created_at": "2025-01-09T12:00:00.000Z",
"inputMethod": "text",
"hasMedia": false
},
{
"role": "user",
"text": "I need assistance",
"created_at": "2025-01-09T12:00:15.000Z",
"inputMethod": "speech",
"hasMedia": true,
"mediaType": "audio"
// Note: Media URLs are NOT included for security/size reasons
}
],
"createdAt": "2025-01-09T11:55:00.000Z",
"startedAt": "2025-01-09T11:59:00.000Z",
"finishedAt": "2025-01-09T12:10:00.000Z",
"question_index": 5,
"digest": {
// Optional: only present when conversation is already DIGESTED at the
// time CONVERSATION_COMPLETE fires (e.g. when re-opening a completed
// interview). Normally the digest arrives later via DIGEST_READY.
"summary": "...",
"themes": ["..."],
"opportunities": ["..."],
"quotes": [{ "quote": "...", "score": 85 }]
}
}
}
Field notes:
messages[].inputMethod (singular) ā how the participant provided this message: text, speech, video, or image. This is the per-message field and is distinct from the inputMethods (plural) URL parameter used to restrict which methods participants may use.messages[].hasMedia / mediaType ā indicate whether media was recorded (audio, video, image). The actual media URLs are intentionally omitted from postMessage payloads for size/security reasons; retrieve them via the Tellet dashboard or export.createdAt / startedAt / finishedAt ā ISO 8601 strings, or null when a timestamp is not available (including fallback payloads). Always guard before parsing: data.finishedAt ? new Date(data.finishedAt) : null.error ā present only when the transcript could not be loaded in-browser. In that case messages is empty and metadata contains only the identifiers from the interview URL (such as participant_id); check this field before treating the payload as a real transcript, and retrieve the conversation by conversationId from the Tellet dashboard or export instead.Security Features:
Example Usage:
window.addEventListener('message', (event) => {
if (event.data.type === 'CONVERSATION_COMPLETE') {
const { conversationId, messages, metadata, startedAt, finishedAt } = event.data.data;
// Store conversation ID for later retrieval
document.getElementById('conversationId').value = conversationId;
// Count messages by type
const userMessages = messages.filter((m) => m.role === 'user').length;
console.log(`Participant sent ${userMessages} responses`);
// Send to your analytics
sendToAnalytics({
participant_id: metadata.participant_id,
message_count: messages.length,
duration: calculateDuration(startedAt, finishedAt),
});
}
});
Purpose: Provides AI-generated analysis and insights when the conversation digest completes (a short time after the interview finishes).
When Fired: After the conversation is analysed by AI (fires AFTER CONVERSATION_COMPLETE).
Data Structure:
{
"type": "DIGEST_READY",
"data": {
"conversationId": "67c84d28...",
"summary": "Participant expressed frustration with slow support response times.",
"themes": [
"Customer Support",
"Response Time",
"User Frustration"
],
"opportunities": [
"Improve response automation",
"Add self-service options"
],
"quotes": [
{
"quote": "The support takes too long to respond",
"score": 85
},
{
"quote": "I wish there were more self-help resources",
"score": 70
}
]
}
}
Example Usage:
window.addEventListener('message', (event) => {
if (event.data.type === 'DIGEST_READY') {
const { summary, themes, opportunities } = event.data.data;
// Display key insights to survey admin
console.log('AI Summary:', summary);
console.log('Themes:', themes.join(', '));
// Store for later analysis
document.getElementById('digest').value = JSON.stringify(event.data.data);
}
});
When a participant completes an interview, events fire in this order:
1. STATUS_CHANGE (in_progress) - Multiple times as questions are answered
2. STATUS_CHANGE (done) - Interview complete
3. CONVERSATION_COMPLETE - Full conversation data
4. END - Legacy message (backward compatibility) - safe to advance the survey
5. STATUS_CHANGE (analyzing) - AI digest started
6. STATUS_CHANGE (digested) - Analysis complete (a short while later)
7. DIGEST_READY - AI insights available
Ordering guarantee: CONVERSATION_COMPLETE always fires before END. Persist the transcript payload in your CONVERSATION_COMPLETE handler; by the time your END handler advances the survey, the data is already stored. In the rare case the transcript cannot be loaded, CONVERSATION_COMPLETE still fires with messages: [] and an error field, followed by END ā store the conversationId and retrieve the transcript from the Tellet dashboard or export instead.
These message types remain supported for backward compatibility:
Tellet sends INPUT messages to set attributes on the iframe dynamically. Your survey can store conversation-specific data this way.
{
"type": "INPUT",
"data": {
"conversation_id": "abc123",
"progress": "3/10"
}
}
Simple completion signal. It fires after CONVERSATION_COMPLETE ā when END arrives, the transcript has already been delivered, so it is safe to advance the survey.
{
"type": "END"
}
Default Handler:
if (event.data.type === 'END') {
const questionNumber = frameId.replace('telletFrame', '');
document.getElementById('q' + questionNumber).click();
}
When Tellet requests stored data, respond with iframe attributes.
if (event.data.type === 'OUTPUT') {
const attributes = {};
Array.from(document.getElementById(frameId).attributes).forEach((attr) => {
attributes[attr.name] = attr.value;
});
document.getElementById(frameId).contentWindow.postMessage(attributes, '*');
}
<script>
window.addEventListener('message', (event) => {
let frameId = null;
const iframes = window.document.getElementsByTagName('iframe');
// Identify which iframe sent the message
for (let i = 0; i < iframes.length; i++) {
if (event.source === iframes[i].contentWindow) {
frameId = iframes[i].id;
break;
}
}
if (!frameId) return;
// ===== ENHANCED MESSAGE TYPES =====
// Real-time status updates
if (event.data.type === 'STATUS_CHANGE') {
const { status, progress, conversationId } = event.data.data;
console.log(`Status: ${status}, Progress: ${progress.count}/${progress.limit}`);
// Update your UI with progress information
}
// Complete conversation data - fires BEFORE END, so persist it here and
// it is guaranteed to be stored before your survey advances
if (event.data.type === 'CONVERSATION_COMPLETE') {
const { conversationId, messages, error } = event.data.data;
console.log(`Conversation ${conversationId} complete with ${messages.length} messages`);
// Persist the payload where your survey platform can read it back,
// e.g. as an attribute on the iframe or in a hidden form field:
document
.getElementById(frameId)
.setAttribute('conversation_complete_payload', JSON.stringify(event.data.data));
if (error) {
// Transcript could not be loaded in-browser; store conversationId and
// retrieve the conversation from the Tellet dashboard or export.
console.warn(`Transcript unavailable in-browser for ${conversationId}: ${error}`);
}
}
// AI-generated insights
if (event.data.type === 'DIGEST_READY') {
const { summary, themes, opportunities, quotes } = event.data.data;
console.log('AI Summary:', summary);
// Display or store analysis results
}
// ===== LEGACY MESSAGE TYPES (Backward Compatibility) =====
if (event.data.type === 'INPUT') {
for (const [attr, value] of Object.entries(event.data.data)) {
document.getElementById(frameId).setAttribute(attr, value);
}
}
if (event.data.type === 'END') {
// CONVERSATION_COMPLETE has already been delivered at this point -
// safe to advance the survey
const questionNumber = frameId.replace('telletFrame', '');
document.getElementById('q' + questionNumber).click();
}
if (event.data.type === 'OUTPUT') {
const attributes = {};
Array.from(document.getElementById(frameId).attributes).forEach((attr) => {
attributes[attr.name] = attr.value;
});
document.getElementById(frameId).contentWindow.postMessage(attributes, '*');
}
});
</script>
Good news: No migration needed! The enhanced message types are additive only.
Backward Compatibility:
END message still fires on completion (after CONVERSATION_COMPLETE, so the transcript is always delivered before your survey advances)INPUT/OUTPUT messages continue to workRecommended Adoption Path:
?language=Spanish; Language Selector: Hidden; Accept Terms: Shown; Use Case: Language known, need consent?skipConsent=true; Language Selector: Shown; Accept Terms: Hidden; Use Case: Consent in survey, language varies?language=Spanish&skipConsent=true; Language Selector: Hidden; Accept Terms: Hidden; Use Case: Survey embeddingRecommendation: Use ?language=Spanish&skipConsent=true for survey embedding since participants already consented to survey.
style="width: 100%; height: 600px; border: none;"
<!-- Interview 1 -->
<iframe id="telletFrame1" src="...PROJECT_ID_1..."></iframe>
<input type="checkbox" id="q1" style="display:none;" />
<!-- Interview 2 -->
<iframe id="telletFrame2" src="...PROJECT_ID_2..."></iframe>
<input type="checkbox" id="q2" style="display:none;" />
<!-- Communication script (once per page) -->
<script>
window.addEventListener('message', (event) => {
// ... same script as before ...
});
</script>
Requirements:
telletFrame1, telletFrame2q1, q2Basic functionality:
Edge cases:
Cross-browser/device:
Always test with ?environment=sandbox:
Critical: Remove environment=sandbox for live surveys.
Join datasets using participant_id:
Survey Platform: ResponseID: ABC123, Q1_Score: 3, Q2_Segment: Premium
Tellet Export: participant_id: ABC123, conversation_text: "I rated 3 because...", plus the AI summary and categories
Join on: ResponseID = participant_id
telletFrame1 ā q1), check JavaScript errors?skipConsent=true&language=English, verify URL formattingallow="camera; microphone; autoplay;", use HTTPS, check browser permissions"null"), no Q&A JSON; Solutions: Persist the CONVERSATION_COMPLETE payload in your message handler (see Complete Communication Script) ā the transcript is delivered only via that event, never via the iframe attributes. CONVERSATION_COMPLETE always arrives before END, so store it before advancing the survey"null" values in iframe attributes ā Symptoms: Keys like chat_settings, current_project_language, screening_state show the string "null"; Solutions: This is normal: Tellet clears its session storage when the interview completes, and the cleanup writes arrive as null values. It is a sign the interview finished ā not an error. Capture the transcript via CONVERSATION_COMPLETE insteadBrowser policies require HTTPS for camera/microphone access. Ensure survey platform uses HTTPS.
Some platforms may block iframes. Check browser DevTools Console for CSP errors. Contact platform to whitelist chat.tellet.ai.
participant_id?participant_id={{ID}}&score={{Q1}}&skipConsent=true&language=English?language={{Q1}}&skipConsent=true&participant_id={{ID}}participant_id - Unique respondent identifierlanguage - Interview language (recommended)skipConsent=true - Skip consent screen (recommended)%%variableName%%participant_id