Wistia
"Wistia: video hosting for marketing, upload API, customizable player, engagement analytics, chapters, CTAs, heatmaps, A/B testing, SEO metadata"
Wistia is a video hosting platform designed specifically for marketing and business use cases. Unlike general-purpose video CDNs, Wistia focuses on viewer engagement tracking, lead generation through turnstile email gates, calls-to-action (CTAs), and detailed analytics including heatmaps showing exactly how viewers watch. The core data model centers on Projects (organizational folders) containing Medias (videos and audio files). Every media gets a customizable player with engagement features built in. The API is REST-based with Bearer token authentication. Design your integration around the media lifecycle — upload, customize, embed, and analyze — and use Wistia's analytics API to feed engagement data back into your marketing stack. ## Key Points - Use the `hashed_id` (not the numeric `id`) for all embed codes and public-facing URLs. - Add turnstile email gates strategically — at the beginning for gated content, or partway through to capture engaged viewers. - Set meaningful titles and descriptions on every video for Wistia's automatic SEO schema generation. - Use chapters for any video over 2 minutes to improve viewer navigation and engagement. - Track the `percentwatchedchanged` event to measure true engagement, not just play counts. - Upload captions for accessibility and SEO benefits; Wistia indexes caption text for search. - Do not use numeric media IDs in embed codes; always use the `hashed_id` string. - Do not skip setting a project for uploaded videos; unorganized media becomes unmanageable at scale. - Do not place multiple Wistia players on a single page without lazy loading; each player loads its own assets. - Do not ignore the `status` field after upload; serve placeholder content until the media reaches `ready` state. - Do not rely solely on play count as an engagement metric; use the engagement score (average percent watched) for meaningful analysis. - Do not hardcode the Wistia embed script URLs; use the official embed patterns so updates are applied automatically. ## Quick Example ``` WISTIA_API_TOKEN=your-api-access-token ```
skilldb get video-services-skills/WistiaFull skill: 329 linesWistia
Core Philosophy
Wistia is a video hosting platform designed specifically for marketing and business use cases. Unlike general-purpose video CDNs, Wistia focuses on viewer engagement tracking, lead generation through turnstile email gates, calls-to-action (CTAs), and detailed analytics including heatmaps showing exactly how viewers watch. The core data model centers on Projects (organizational folders) containing Medias (videos and audio files). Every media gets a customizable player with engagement features built in. The API is REST-based with Bearer token authentication. Design your integration around the media lifecycle — upload, customize, embed, and analyze — and use Wistia's analytics API to feed engagement data back into your marketing stack.
Setup
Wistia uses a simple Bearer token API. Generate an access token from your Wistia account settings:
const WISTIA_API_TOKEN = process.env.WISTIA_API_TOKEN!;
async function wistiaRequest<T>(
path: string,
options: RequestInit = {}
): Promise<T> {
const res = await fetch(`https://api.wistia.com/v1${path}`, {
...options,
headers: {
Authorization: `Bearer ${WISTIA_API_TOKEN}`,
"Content-Type": "application/json",
...options.headers,
},
});
if (!res.ok) {
const error = await res.text();
throw new Error(`Wistia API error ${res.status}: ${error}`);
}
return res.json();
}
Environment variables required:
WISTIA_API_TOKEN=your-api-access-token
Key Techniques
Uploading Videos
interface WistiaMedia {
id: number;
hashed_id: string;
name: string;
status: string; // "queued", "processing", "ready", "failed"
duration: number;
thumbnail: { url: string; width: number; height: number };
assets: Array<{ type: string; url: string; width: number; height: number }>;
}
// Upload from URL
async function uploadFromUrl(videoUrl: string, projectId: string, name: string): Promise<WistiaMedia> {
return wistiaRequest<WistiaMedia>("/medias.json", {
method: "POST",
body: JSON.stringify({
url: videoUrl,
project_id: projectId,
name,
}),
});
}
// Upload from file (multipart)
async function uploadFile(file: Buffer, filename: string, projectId: string): Promise<WistiaMedia> {
const formData = new FormData();
formData.append("file", new Blob([file]), filename);
formData.append("project_id", projectId);
const res = await fetch("https://upload.wistia.com/", {
method: "POST",
headers: { Authorization: `Bearer ${WISTIA_API_TOKEN}` },
body: formData,
});
if (!res.ok) throw new Error(`Upload failed: ${res.status}`);
return res.json();
}
Project Management
interface WistiaProject {
id: number;
hashed_id: string;
name: string;
mediaCount: number;
created: string;
}
async function createProject(name: string): Promise<WistiaProject> {
return wistiaRequest<WistiaProject>("/projects.json", {
method: "POST",
body: JSON.stringify({ name }),
});
}
async function listProjects(): Promise<WistiaProject[]> {
return wistiaRequest<WistiaProject[]>("/projects.json");
}
async function getProjectVideos(projectId: string): Promise<WistiaMedia[]> {
const project = await wistiaRequest<WistiaProject & { medias: WistiaMedia[] }>(
`/projects/${projectId}.json`
);
return project.medias;
}
Embedding the Player
// Standard embed (recommended for most use cases)
function getEmbedCode(hashedId: string, options: Record<string, string | boolean> = {}): string {
const params = new URLSearchParams();
for (const [key, value] of Object.entries(options)) {
params.set(key, String(value));
}
return `<script src="https://fast.wistia.com/embed/medias/${hashedId}.jsonp" async></script>
<script src="https://fast.wistia.com/assets/external/E-v1.js" async></script>
<div class="wistia_embed wistia_async_${hashedId} ${params.toString()}"
style="width:100%;aspect-ratio:16/9;"> </div>`;
}
// React component with the Wistia Player API
function WistiaPlayer({ hashedId }: { hashedId: string }) {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
// Load Wistia scripts
const script = document.createElement("script");
script.src = `https://fast.wistia.com/embed/medias/${hashedId}.jsonp`;
script.async = true;
document.head.appendChild(script);
const ev1 = document.createElement("script");
ev1.src = "https://fast.wistia.com/assets/external/E-v1.js";
ev1.async = true;
document.head.appendChild(ev1);
// Access Wistia Player API when ready
(window as any)._wq = (window as any)._wq || [];
(window as any)._wq.push({
id: hashedId,
onReady(video: any) {
video.bind("play", () => console.log("Video started"));
video.bind("percentwatchedchanged", (pct: number) => {
if (pct >= 0.5) console.log("50% watched");
});
video.bind("end", () => console.log("Video finished"));
},
});
return () => {
script.remove();
ev1.remove();
};
}, [hashedId]);
return (
<div
ref={containerRef}
className={`wistia_embed wistia_async_${hashedId}`}
style={{ width: "100%", aspectRatio: "16/9" }}
/>
);
}
Customizing the Player
async function customizePlayer(hashedId: string) {
return wistiaRequest(`/medias/${hashedId}/customizations.json`, {
method: "PUT",
body: JSON.stringify({
playerColor: "636155",
playButton: true,
smallPlayButton: true,
playbar: true,
volumeControl: true,
fullscreenButton: true,
controlsVisibleOnLoad: true,
autoPlay: false,
muted: false,
// Turnstile email collector
plugin: {
"requireEmail-v1": {
topText: "Enter your email to watch",
bottomText: "We respect your privacy",
time: 0, // show at beginning; use -1 for end
required: true,
},
// Call-to-action at end of video
"postRoll-v1": {
text: "Want to learn more?",
link: "https://example.com/demo",
autoSize: true,
},
// Chapters
"chapters-v1": {
chapterList: [
{ title: "Introduction", time: 0 },
{ title: "Key Features", time: 45 },
{ title: "Demo", time: 120 },
{ title: "Pricing", time: 210 },
],
},
},
}),
});
}
Engagement Analytics
interface WistiaStats {
id: number;
name: string;
play_count: number;
hours_watched: number;
engagement: number; // 0-1, average % of video watched
visitors: number;
}
// Get stats for a specific video
async function getMediaStats(mediaId: string): Promise<WistiaStats> {
return wistiaRequest<WistiaStats>(
`/medias/${mediaId}/stats.json`
);
}
// Get project-level stats
async function getProjectStats(projectId: string) {
return wistiaRequest(`/projects/${projectId}/stats.json`);
}
// Get account-wide stats
async function getAccountStats() {
return wistiaRequest("/stats/account.json");
}
// Get visitor-level engagement data
async function getVisitorSessions(mediaId: string) {
return wistiaRequest(
`/stats/events.json?media_id=${mediaId}&per_page=100`
);
}
Captions and Accessibility
// Order automatic captions
async function orderCaptions(hashedId: string) {
return wistiaRequest(`/medias/${hashedId}/captions/purchase.json`, {
method: "POST",
});
}
// Upload custom captions (SRT format)
async function uploadCaptions(hashedId: string, srtContent: string, languageCode = "eng") {
return wistiaRequest(`/medias/${hashedId}/captions.json`, {
method: "POST",
body: JSON.stringify({
caption_file: Buffer.from(srtContent).toString("base64"),
language: languageCode,
}),
});
}
// List available captions
async function listCaptions(hashedId: string) {
return wistiaRequest(`/medias/${hashedId}/captions.json`);
}
SEO Metadata
// Update video metadata for search engine indexing
async function updateMediaMetadata(hashedId: string, metadata: {
name?: string;
description?: string;
seoTitle?: string;
seoDescription?: string;
}) {
return wistiaRequest(`/medias/${hashedId}.json`, {
method: "PUT",
body: JSON.stringify({
name: metadata.name,
description: metadata.description,
}),
});
}
// Wistia automatically generates JSON-LD structured data for embedded videos.
// Ensure your page includes the embed code and the video has a description set.
Best Practices
- Use the
hashed_id(not the numericid) for all embed codes and public-facing URLs. - Add turnstile email gates strategically — at the beginning for gated content, or partway through to capture engaged viewers.
- Set meaningful titles and descriptions on every video for Wistia's automatic SEO schema generation.
- Use chapters for any video over 2 minutes to improve viewer navigation and engagement.
- Track the
percentwatchedchangedevent to measure true engagement, not just play counts. - Upload captions for accessibility and SEO benefits; Wistia indexes caption text for search.
Anti-Patterns
- Do not use numeric media IDs in embed codes; always use the
hashed_idstring. - Do not skip setting a project for uploaded videos; unorganized media becomes unmanageable at scale.
- Do not place multiple Wistia players on a single page without lazy loading; each player loads its own assets.
- Do not ignore the
statusfield after upload; serve placeholder content until the media reachesreadystate. - Do not rely solely on play count as an engagement metric; use the engagement score (average percent watched) for meaningful analysis.
- Do not hardcode the Wistia embed script URLs; use the official embed patterns so updates are applied automatically.
Install this skill directly: skilldb add video-services-skills
Related Skills
Amazon IVS
"Amazon Interactive Video Service: low-latency live streaming, real-time stages, chat, stream recording, channel management, viewer analytics"
Api.video
"api.video: video hosting API, upload, live streaming, player customization, analytics, webhooks"
Bunny Stream
"Bunny.net Stream: video hosting, HLS delivery, direct uploads, video collections, thumbnail generation, token authentication, webhook events"
Cloudflare Stream
"Cloudflare Stream: video hosting, adaptive streaming, direct upload, watermarks, signed URLs, Workers integration"
LiveKit
"LiveKit: real-time video/audio, WebRTC, rooms, tracks, screen sharing, recording, React components"
Mux Video
"Mux: video hosting, HLS streaming, upload API, playback URLs, thumbnails, analytics, Mux Player, Next.js integration"