> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mavera.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Ad Creative Video Analysis

> Pull video ad creatives from Meta, upload to Mavera Assets, and run Video Analysis for emotional, cognitive, and behavioral scoring

### Scenario

You're running video ads across Facebook and Instagram but have no systematic way to evaluate why some creatives outperform others. This job pulls video ad creatives from your ad account, uploads them to Mavera Assets, and runs Video Analysis on each. The output gives you emotional, cognitive, and behavioral scoring — the "why" behind creative performance that CPM and CTR alone can't explain.

### Architecture

```mermaid theme={"dark"}
flowchart LR
    A["Meta GET adcreatives (video_id)"] --> B[Download video files] --> C["POST /api/v1/assets"] --> D["POST /api/v1/video-analyses"] --> E["Poll GET /api/v1/video-analyses/{id}"] --> F[Scoring report]
```

### Code

<CodeGroup>
  ```python Python theme={"dark"}
  import os, requests, time, tempfile

  META = os.environ["META_ACCESS_TOKEN"]
  ACCT = os.environ["META_AD_ACCOUNT_ID"]
  MV = os.environ["MAVERA_API_KEY"]
  GRAPH = "https://graph.facebook.com/v24.0"
  MB = "https://app.mavera.io/api/v1"
  MH = {"Authorization": f"Bearer {MV}"}

  # 1. Pull video creatives
  creatives = requests.get(
      f"{GRAPH}/{ACCT}/adcreatives",
      params={
          "access_token": META,
          "fields": "id,name,title,video_id,thumbnail_url,object_story_spec",
          "limit": 50,
      },
  ).json().get("data", [])

  video_creatives = [c for c in creatives if c.get("video_id")]
  print(f"Found {len(video_creatives)} video creatives")

  results = []
  for vc in video_creatives[:10]:
      vid = vc["video_id"]

      # 2. Get video source URL
      video_info = requests.get(
          f"{GRAPH}/{vid}",
          params={"access_token": META, "fields": "source,title,length"},
      ).json()
      source_url = video_info.get("source")
      if not source_url:
          print(f"  Skipping {vid} — no source URL (check permissions)")
          continue

      # 3. Download video to temp file
      vid_resp = requests.get(source_url, stream=True)
      vid_resp.raise_for_status()
      tmp = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False)
      for chunk in vid_resp.iter_content(8192):
          tmp.write(chunk)
      tmp.close()

      # 4. Upload to Mavera Assets
      with open(tmp.name, "rb") as f:
          asset = requests.post(
              f"{MB}/assets",
              headers=MH,
              files={"file": (f"{vid}.mp4", f, "video/mp4")},
          ).json()
      asset_id = asset["id"]
      os.unlink(tmp.name)

      # 5. Start Video Analysis
      analysis = requests.post(
          f"{MB}/video-analyses",
          headers={**MH, "Content-Type": "application/json"},
          json={"asset_id": asset_id, "name": vc.get("name", f"Meta Creative {vid}")},
      ).json()
      analysis_id = analysis["id"]

      # 6. Poll for completion
      for attempt in range(30):
          time.sleep(10)
          status = requests.get(
              f"{MB}/video-analyses/{analysis_id}", headers=MH
          ).json()
          if status.get("status") == "completed":
              break
          if status.get("status") == "failed":
              print(f"  Analysis failed for {vid}: {status.get('error', 'unknown')}")
              break
      else:
          print(f"  Timeout waiting for analysis of {vid}")
          continue

      results.append({
          "creative_id": vc["id"],
          "creative_name": vc.get("name", "Untitled"),
          "video_id": vid,
          "duration": video_info.get("length"),
          "analysis_id": analysis_id,
          "scores": status.get("scores", {}),
      })
      print(f"  ✓ {vc.get('name', vid)}: emotional={status.get('scores',{}).get('emotional','N/A')}")
      time.sleep(1)

  print(f"\nAnalyzed {len(results)} video creatives")
  for r in results:
      s = r["scores"]
      print(f"  {r['creative_name']}: "
            f"emotional={s.get('emotional','N/A')} "
            f"cognitive={s.get('cognitive','N/A')} "
            f"behavioral={s.get('behavioral','N/A')}")
  ```

  ```javascript JavaScript theme={"dark"}
  const META = process.env.META_ACCESS_TOKEN;
  const ACCT = process.env.META_AD_ACCOUNT_ID;
  const MV = process.env.MAVERA_API_KEY;
  const GRAPH = "https://graph.facebook.com/v24.0";
  const MB = "https://app.mavera.io/api/v1";
  const MH = { Authorization: `Bearer ${MV}` };

  // 1. Pull video creatives
  const creatives = await fetch(
    `${GRAPH}/${ACCT}/adcreatives?access_token=${META}&fields=id,name,title,video_id,thumbnail_url,object_story_spec&limit=50`
  ).then(r => r.json()).then(d => d.data || []);

  const videoCreatives = creatives.filter(c => c.video_id);
  console.log(`Found ${videoCreatives.length} video creatives`);

  const results = [];
  for (const vc of videoCreatives.slice(0, 10)) {
    const vid = vc.video_id;

    // 2. Get video source URL
    const videoInfo = await fetch(
      `${GRAPH}/${vid}?access_token=${META}&fields=source,title,length`
    ).then(r => r.json());
    if (!videoInfo.source) { console.log(`  Skipping ${vid}`); continue; }

    // 3. Download video
    const vidResp = await fetch(videoInfo.source);
    const vidBuffer = Buffer.from(await vidResp.arrayBuffer());

    // 4. Upload to Mavera Assets
    const form = new FormData();
    form.append("file", new Blob([vidBuffer], { type: "video/mp4" }), `${vid}.mp4`);
    const asset = await fetch(`${MB}/assets`, {
      method: "POST", headers: MH, body: form,
    }).then(r => r.json());

    // 5. Start Video Analysis
    const analysis = await fetch(`${MB}/video-analyses`, {
      method: "POST",
      headers: { ...MH, "Content-Type": "application/json" },
      body: JSON.stringify({ asset_id: asset.id, name: vc.name || `Meta Creative ${vid}` }),
    }).then(r => r.json());

    // 6. Poll for completion
    let status;
    for (let i = 0; i < 30; i++) {
      await new Promise(r => setTimeout(r, 10000));
      status = await fetch(`${MB}/video-analyses/${analysis.id}`, { headers: MH }).then(r => r.json());
      if (status.status === "completed" || status.status === "failed") break;
    }

    if (status?.status === "completed") {
      results.push({
        creative_id: vc.id, creative_name: vc.name || "Untitled",
        video_id: vid, duration: videoInfo.length,
        analysis_id: analysis.id, scores: status.scores || {},
      });
      console.log(`  ✓ ${vc.name || vid}: emotional=${status.scores?.emotional ?? "N/A"}`);
    }
    await new Promise(r => setTimeout(r, 1000));
  }

  console.log(`\nAnalyzed ${results.length} video creatives`);
  results.forEach(r => {
    const s = r.scores;
    console.log(`  ${r.creative_name}: emotional=${s.emotional ?? "N/A"} cognitive=${s.cognitive ?? "N/A"} behavioral=${s.behavioral ?? "N/A"}`);
  });
  ```
</CodeGroup>

### Example Output

```json theme={"dark"}
{
  "analyzed": 6,
  "creatives": [
    {
      "creative_name": "Summer Sale Hero — 30s",
      "video_id": "1234567890",
      "duration": 30,
      "scores": {
        "emotional": 8.2,
        "cognitive": 6.5,
        "behavioral": 7.8,
        "attention_curve": [9.1, 8.5, 7.2, 6.8, 8.9],
        "key_moments": [
          { "timestamp": 3.2, "type": "hook", "strength": 9.1 },
          { "timestamp": 18.5, "type": "cta", "strength": 8.9 }
        ]
      }
    },
    {
      "creative_name": "Testimonial — Real Users",
      "video_id": "0987654321",
      "duration": 45,
      "scores": {
        "emotional": 9.0,
        "cognitive": 7.1,
        "behavioral": 6.3
      }
    }
  ]
}
```

### Error Handling

<AccordionGroup>
  <Accordion title="No source URL on video">The `source` field requires the `ads_read` permission and the video must be owned by your ad account. Videos from Page posts need `pages_read_engagement`.</Accordion>
  <Accordion title="Video download fails (403)">Source URLs are signed and expire. Download immediately after fetching the URL — don't cache URLs for later.</Accordion>
  <Accordion title="Analysis stuck in processing">Large videos (>2 min) can take 2–5 minutes. The polling loop allows up to 5 minutes. For longer videos, increase the attempt count or switch to webhooks if Mavera supports them.</Accordion>
  <Accordion title="Rate limit (9,000 points/300s)">Each `GET` to Graph API costs 1 point. With 10 creatives × 2 calls each = 20 points — well within limits. For 500+ creatives, batch with `?ids=` param.</Accordion>
</AccordionGroup>

<CardGroup cols={2}>
  <Card title="Meta Ads Integration" icon="meta" href="/integrations/meta-ads">
    All Meta Ads jobs
  </Card>

  <Card title="Video Analysis" icon="video" href="/features/video-analysis">
    Full guide to Mavera Video Analysis
  </Card>
</CardGroup>
