Skip to main content
Technology & EngineeringVideo Services329 lines

Wistia

"Wistia: video hosting for marketing, upload API, customizable player, engagement analytics, chapters, CTAs, heatmaps, A/B testing, SEO metadata"

Quick Summary24 lines
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 lines
Paste into your CLAUDE.md or agent config

Wistia

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;">&nbsp;</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 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.

Anti-Patterns

  • 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.

Install this skill directly: skilldb add video-services-skills

Get CLI access →