Video Clip API

The Meraki MV Video Clip API enables programmatic retrieval of precise video segments from your MV cameras using specific timestamps. Available on Gen 2 and Gen 3 cameras with firmware 7.2+.

Overview

Request on-demand video clips without continuous streaming infrastructure. Perfect for:

  • Security event documentation
  • Incident investigation tools
  • Automated alert workflows
  • Integration with third-party systems

Key Benefits

  • Precision: Request exact time windows (down to the second)
  • Speed: Clips generate rapidly and are delivered via temporary URLs
  • Simplicity: Standard REST API with Meraki API key authentication
  • Flexibility: Up to 5-minute clips per request

Endpoint

GET /devices/{serial}/camera/clip

Retrieves a video clip from a specific camera for a given time range.

Path Parameters

Parameter Type Description
serial string Required. The serial number of the target MV camera

Query Parameters

Parameter Type Description
startTimestamp string Required. ISO8601-formatted start time (UTC). Example: 2026-03-12T14:30:00.000Z
endTimestamp string Required. ISO8601-formatted end time (UTC). Must be within 5 minutes of start time. Example: 2026-03-12T14:32:00.000Z

Request Headers

Authorization: Bearer {api_key}
Accept: application/json

Response

Success (200 OK)

{
  "url": "https://spn49.meraki.com/stream/jpeg/snapshot/{token}"
}

The url field contains a temporary URL to the video clip in MPEG-TS format. This URL will expire, so download or stream the content immediately.

Error Responses

Status Code Description
400 Bad Request - Invalid timestamp format or duration exceeds 5 minutes
404 Not Found - Camera serial not found or not an MV camera
429 Too Many Requests - Rate limit exceeded

Example Requests

cURL

curl --request GET \
  --url 'https://api.meraki.com/api/v1/devices/Q2XX-XXXX-XXXX/camera/clip?startTimestamp=2026-03-12T14:30:00.000Z&endTimestamp=2026-03-12T14:32:00.000Z' \
  --header 'Authorization: Bearer {api_key}' \
  --header 'Accept: application/json'

Python

import requests
from datetime import datetime, timedelta

# Configuration
API_KEY = "your_api_key_here"
CAMERA_SERIAL = "Q2XX-XXXX-XXXX"
BASE_URL = "https://api.meraki.com/api/v1"

# Request a 2-minute clip from 5 minutes ago
end_time = datetime.utcnow() - timedelta(minutes=5)
start_time = end_time - timedelta(minutes=2)

# Format timestamps
params = {
    "startTimestamp": start_time.strftime('%Y-%m-%dT%H:%M:%S.000Z'),
    "endTimestamp": end_time.strftime('%Y-%m-%dT%H:%M:%S.000Z')
}

# Make request
response = requests.get(
    f"{BASE_URL}/devices/{CAMERA_SERIAL}/camera/clip",
    headers={
        "Authorization": f"Bearer {API_KEY}",
        "Accept": "application/json"
    },
    params=params
)

if response.status_code == 200:
    clip_url = response.json()["url"]

    # Download the clip
    video_response = requests.get(clip_url)
    with open("clip.ts", "wb") as f:
        f.write(video_response.content)

    print("Clip downloaded successfully")
else:
    print(f"Error: {response.status_code} - {response.text}")

JavaScript (Node.js)

const axios = require('axios');
const fs = require('fs');

const API_KEY = 'your_api_key_here';
const CAMERA_SERIAL = 'Q2XX-XXXX-XXXX';
const BASE_URL = 'https://api.meraki.com/api/v1';

// Request a 2-minute clip from 5 minutes ago
const endTime = new Date(Date.now() - 5 * 60 * 1000);
const startTime = new Date(endTime - 2 * 60 * 1000);

async function getVideoClip() {
  try {
    // Request clip
    const response = await axios.get(
      `${BASE_URL}/devices/${CAMERA_SERIAL}/camera/clip`,
      {
        headers: {
          'Authorization': `Bearer ${API_KEY}`,
          'Accept': 'application/json'
        },
        params: {
          startTimestamp: startTime.toISOString(),
          endTimestamp: endTime.toISOString()
        }
      }
    );

    const clipUrl = response.data.url;

    // Download the clip
    const videoResponse = await axios.get(clipUrl, {
      responseType: 'arraybuffer'
    });

    fs.writeFileSync('clip.ts', videoResponse.data);
    console.log('Clip downloaded successfully');

  } catch (error) {
    console.error('Error:', error.response?.status, error.response?.data);
  }
}

getVideoClip();

Implementation Guide

Video Format Handling

The API returns MPEG-TS (.ts) files. Depending on your use case, you may need to process these files:

Option 1: Convert to MP4 (Server-Side)

Use FFmpeg to convert for broader compatibility:

ffmpeg -i clip.ts -c copy -bsf:a aac_adtstoasc clip.mp4

Option 2: Stream with HLS.js (Browser)

For web applications, use HLS.js or mux.js to transmux to fMP4:

import Hls from 'hls.js';

const video = document.getElementById('video');
const hls = new Hls();
hls.loadSource(clipUrl);
hls.attachMedia(video);

Option 3: Backend Proxy Pattern

For the most robust solution:

  1. Backend calls Meraki API and proxies the .ts stream
  2. Backend converts to MP4 on-the-fly or serves .ts with proper CORS headers
  3. Frontend receives a clean URL from your backend

Best Practices

Timing Considerations

  • Latency: Allow 30-60 seconds of lag behind live time for clip availability
  • Pre/Post Roll: Add buffer time (30s before/after) for event context
  • Max Duration: 5 minutes per request; chain multiple requests for longer periods
# Example: Request clip with 30s pre-roll and 30s post-roll
event_time = datetime.fromisoformat("2026-03-12T14:31:00Z")
start_time = event_time - timedelta(seconds=30)  # 30s before
end_time = event_time + timedelta(seconds=30)    # 30s after

Error Handling

Implement exponential backoff for rate limits:

import time

def request_clip_with_retry(serial, start, end, max_retries=3):
    for attempt in range(max_retries):
        response = requests.get(
            f"{BASE_URL}/devices/{serial}/camera/clip",
            headers=headers,
            params={"startTimestamp": start, "endTimestamp": end}
        )

        if response.status_code == 200:
            return response.json()["url"]
        elif response.status_code == 429:
            # Rate limited - wait and retry
            wait_time = 2 ** attempt  # Exponential backoff
            time.sleep(wait_time)
        else:
            # Other error - don't retry
            response.raise_for_status()

    raise Exception("Max retries exceeded")

Security

  • Never expose API keys client-side: Use a backend proxy
  • Validate timestamps: Ensure they're within retention period
  • Download immediately: URLs are temporary and will expire
  • Sanitize inputs: Validate serial numbers and timestamps before API calls

Integration Examples

Security Incident Workflow

def document_security_incident(camera_serial, incident_time, pre_roll=30, post_roll=30):
    """
    Capture video evidence for a security incident.

    Args:
        camera_serial: MV camera serial number
        incident_time: datetime object of when incident occurred
        pre_roll: seconds before incident to include
        post_roll: seconds after incident to include

    Returns:
        Path to downloaded video clip
    """
    start = incident_time - timedelta(seconds=pre_roll)
    end = incident_time + timedelta(seconds=post_roll)

    # Request clip
    response = requests.get(
        f"{BASE_URL}/devices/{camera_serial}/camera/clip",
        headers={"Authorization": f"Bearer {API_KEY}"},
        params={
            "startTimestamp": start.strftime('%Y-%m-%dT%H:%M:%S.000Z'),
            "endTimestamp": end.strftime('%Y-%m-%dT%H:%M:%S.000Z')
        }
    )

    clip_url = response.json()["url"]

    # Download and save
    video = requests.get(clip_url).content
    filename = f"incident_{incident_time.strftime('%Y%m%d_%H%M%S')}.ts"

    with open(filename, "wb") as f:
        f.write(video)

    return filename

Multi-Camera Coverage

def get_multi_camera_coverage(camera_serials, event_time, duration_mins=2):
    """
    Request clips from multiple cameras for the same time window.

    Returns:
        Dict mapping camera serial to clip URL
    """
    clips = {}

    start = event_time - timedelta(minutes=duration_mins/2)
    end = event_time + timedelta(minutes=duration_mins/2)

    for serial in camera_serials:
        try:
            response = requests.get(
                f"{BASE_URL}/devices/{serial}/camera/clip",
                headers={"Authorization": f"Bearer {API_KEY}"},
                params={
                    "startTimestamp": start.strftime('%Y-%m-%dT%H:%M:%S.000Z'),
                    "endTimestamp": end.strftime('%Y-%m-%dT%H:%M:%S.000Z')
                }
            )

            if response.status_code == 200:
                clips[serial] = response.json()["url"]
        except Exception as e:
            print(f"Failed to get clip from {serial}: {e}")

    return clips

Requirements

Hardware

  • Meraki MV Gen 2 or Gen 3 camera
  • Firmware version 7.2 or newer

Authentication

Rate Limits

  • Standard Meraki API rate limits apply: 5 requests per second per organization
  • Use appropriate retry logic and exponential backoff for 429 errors

Troubleshooting

Common Issues

400 Bad Request - "Invalid timestamp format"

  • Ensure timestamps are in ISO8601 format with UTC timezone
  • Format: YYYY-MM-DDTHH:MM:SS.000Z
  • Verify endTimestamp is after startTimestamp
  • Verify duration is 5 minutes or less

404 Not Found

  • Verify camera serial number is correct
  • Ensure camera is MV (not MS, MR, etc.)
  • Check API key has access to the organization

Empty/Corrupt Video File

  • Verify timestamps are within the camera's retention period
  • Ensure sufficient lag time (30-60s) behind current time
  • Check network connectivity during download

Rate Limit (429)

  • Implement exponential backoff
  • Reduce request frequency
  • Consider caching clips if accessed multiple times