# Clips/Short Videos Mng API

**Author**: Josh S. Sakweli, Backend Lead Team  
**Last Updated**: 2026-06-22  
**Version**: v1.0

**Base URL**: `{base_url}/api/v1/e-social/clip`

**Short Description**: The Clips API powers the short-form vertical video surface on the platform. A clip is not a separate content type — it is a video media item attached to a post, where `shortClip = true`. This API provides the feed, individual clip access, view tracking, and user video profiles. Liking, commenting, and saving a clip are handled by the existing Posts interaction API using the clip's parent `postId`.

**Why Clips Are Not Separate Posts**:

> **Clip** = a `post_media` row where `shortClip = true` and `mediaType = VIDEO`.
> **Post** = the parent container that owns the clip's caption, engagement counters (likes, comments, shares, saves), and attachments.
>
> This design avoids duplicating the entire post infrastructure. A clip inherits everything social from its parent post. The only clip-specific data is the per-media `viewsCount` (each video tracks its own views independently) and the `clip_views` table used for view deduplication. A single post can contain multiple clips — both appear as separate items in the clip feed with their own view counts but share the same caption and like/comment counts.

**Hints**:
- The clip feed is paginated using an opaque cursor — pass `nextCursor` from each response back as `cursor` on the next request
- `POST /{mediaId}/view` is idempotent per user per clip — the first call counts the view, subsequent calls increment a personal `viewCount` only (no double-counting on the public counter)
- Liking a clip: `POST /api/v1/e-social/posts/{postId}/like` — use the `postId` from the clip response
- Commenting on a clip: `POST /api/v1/e-social/posts/{postId}/comments`
- Saving a clip: `POST /api/v1/e-social/posts/{postId}/bookmark`
- `GET /clip/user/{userId}/videos` returns ALL videos from a user (both long and short), not just clips — use this for the profile videos tab
- The `variants` map in the media object is fully dynamic — keys are whatever FileThunder generated for that file (see variants section below)
- View tracking requires authentication — anonymous views are silently ignored (no error returned)
- `currentUserIsPendingCollaborator: true` means the viewer has a pending collaboration invite on this clip — prompt them to accept or decline using the post collaboration endpoints with `postId`
- `collaborators[]` only contains accepted co-authors — pending or declined invites are never exposed to viewers

---

## Standard Response Format

### Success Response Structure
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Operation completed successfully",
  "action_time": "2026-06-22T10:30:45",
  "data": {}
}
```

### Error Response Structure
```json
{
  "success": false,
  "httpStatus": "NOT_FOUND",
  "message": "Clip not found",
  "action_time": "2026-06-22T10:30:45",
  "data": "Clip not found"
}
```

### Standard Response Fields
| Field | Type | Description |
|-------|------|-------------|
| `success` | boolean | `true` for successful operations, `false` for errors |
| `httpStatus` | string | HTTP status name (OK, NOT_FOUND, etc.) |
| `message` | string | Human-readable message describing the result |
| `action_time` | string | ISO 8601 timestamp of when the response was generated |
| `data` | object/string | Response payload on success, error details on failure |

### Standard Error Types
- `400 BAD_REQUEST`: Invalid cursor format or malformed request
- `401 UNAUTHORIZED`: Missing or invalid JWT token
- `404 NOT_FOUND`: Clip not found, deleted, or not published
- `500 INTERNAL_SERVER_ERROR`: Unexpected server error

---

## Core Concepts

### What Is a Clip?

A clip is a `post_media` record that satisfies all three conditions:
- `mediaType = VIDEO`
- `shortClip = true`
- `status = READY` (FileThunder has finished processing)

The parent post must also be `status = PUBLISHED` and `isDeleted = false`. Any clip whose parent post is deleted or unpublished is treated as non-existent — the API returns 404.

### Clip Identity

| Identifier | Used For |
|---|---|
| `id` (media ID) | Clip identity — share links, view tracking, deep links |
| `postId` | Social interactions — like, comment, save, share |

The clip `id` is a UUID that uniquely identifies the video media item. The `postId` is used for all social actions since engagement (likes, comments, saves) belongs to the post, not to the individual video.

### Cursor Pagination

The feed uses cursor-based pagination — not page numbers. The cursor is an opaque base64-encoded string that encodes the position of the last item returned. This guarantees stable pagination even when new clips are added between requests.

```
First call:   GET /clip/feed?limit=10
              → returns 10 clips + nextCursor

Second call:  GET /clip/feed?cursor={nextCursor}&limit=10
              → returns the next 10 clips after the previous batch
```

When `hasMore: false`, the feed is exhausted. Pull-to-refresh starts a new feed from the top (omit the cursor).

### View Deduplication

Views are counted once per user per clip. The dedup table (`clip_views`) enforces a unique constraint on `(mediaId, userId)`.

```
First view by user A on clip X:
  → new row in clip_views
  → post_media.views_count + 1
  → posts.views_count + 1

Second view by user A on clip X:
  → clip_views.view_count + 1 (personal counter only)
  → NO change to post_media.views_count or posts.views_count
```

Anonymous users (no JWT) are ignored — `POST /{mediaId}/view` returns `204` but records nothing.

### Media Variants

The `variants` map is dynamic — keys are whatever FileThunder generated during video processing. For short clips the expected keys are:

| Key | Type | Description |
|---|---|---|
| `master` | URL | HLS adaptive bitrate master playlist — use this for playback |
| `360p_playlist` | URL | HLS playlist for 360p stream |
| `720p_playlist` | URL | HLS playlist for 720p stream |
| `1080p_playlist` | URL | HLS playlist for 1080p stream |
| `poster` | URL | Full-size thumbnail frame (.webp) |
| `thumb` | URL | Small thumbnail (.webp, 300px) |
| `og` | URL | OG/social share thumbnail (1200×630) |
| `blurhash` | string | Blurhash string — not a URL, used for blur placeholder |
| `lqip` | string | Base64 WebP data URI — inline low-quality placeholder |
| `dominant_color` | string | Hex color extracted from the thumbnail (e.g. `#2a2a2a`) |
| `preview_3s.mp4` | URL | 6-second muted preview at 2× speed (360×640) |
| `720p_watermarked.mp4` | URL | Watermarked MP4 (short clips only) |

**Playback recommendation**: use `master` for HLS adaptive streaming. Fall back to `720p_playlist` if the player does not support HLS. Use `lqip` as the placeholder while the video loads, then `poster` once the video is ready to play.

---

## Endpoints

---

## 1. Get Clip Feed

**Purpose**: Returns a paginated feed of short clips, ordered by most recently published. This is the main scroll surface — each call returns the next batch of clips after the provided cursor.

**Endpoint**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `{base_url}/api/v1/e-social/clip/feed`

**Access Level**: 🔒 Protected (Requires valid JWT)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <jwt_token>` |

**Query Parameters**:
| Parameter | Type | Required | Description | Validation | Default |
|-----------|------|----------|-------------|------------|---------|
| `cursor` | string | No | Opaque pagination cursor from the previous response. Omit on the first call. | Valid base64 cursor string | `null` (first page) |
| `limit` | integer | No | Number of clips to return per page | Min: 1, Max: 20 | `10` |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Clip feed fetched",
  "action_time": "2026-06-22T10:30:45",
  "data": {
    "clips": [
      {
        "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
        "postId": "1a2b3c4d-0000-0000-0000-000000000001",
        "author": {
          "id": "aaa-111-bbb-222-ccc",
          "username": "fitnessguru",
          "profilePictureUrl": "https://cdn.nexgate.com/users/aaa/profilepic/medium.webp",
          "profilePictureThumbnailUrl": "https://cdn.nexgate.com/users/aaa/profilepic/thumb.webp",
          "profilePictureBlurhash": "LKO2?U%2Tw=w]~RBVZRi};RPxuwH",
          "verified": true,
          "following": false
        },
        "caption": "morning workout routine 💪 #fitness #gym",
        "media": {
          "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
          "duration": 42,
          "width": 1080,
          "height": 1920,
          "variants": {
            "master": "https://cdn.nexgate.com/posts/aaa/media/xyz/hls/master.m3u8",
            "360p_playlist": "https://cdn.nexgate.com/posts/aaa/media/xyz/hls/360p/360p.m3u8",
            "720p_playlist": "https://cdn.nexgate.com/posts/aaa/media/xyz/hls/720p/720p.m3u8",
            "1080p_playlist": "https://cdn.nexgate.com/posts/aaa/media/xyz/hls/1080p/1080p.m3u8",
            "poster": "https://cdn.nexgate.com/posts/aaa/media/xyz/poster.webp",
            "thumb": "https://cdn.nexgate.com/posts/aaa/media/xyz/thumb.webp",
            "og": "https://cdn.nexgate.com/posts/aaa/media/xyz/og.webp",
            "blurhash": "LKO2?U%2Tw=w]~RBVZRi};RPxuwH",
            "lqip": "data:image/webp;base64,UklGRlYAAABXRUJQ...",
            "dominant_color": "#1a1a2e",
            "preview_3s.mp4": "https://cdn.nexgate.com/posts/aaa/media/xyz/preview_3s.mp4",
            "720p_watermarked.mp4": "https://cdn.nexgate.com/posts/aaa/media/xyz/720p_watermarked.mp4"
          }
        },
        "hashtags": ["fitness", "gym"],
        "mentionedUsers": [
          {
            "id": "ddd-333-eee-444-fff",
            "username": "coachmark",
            "displayName": "Coach Mark",
            "profilePictureUrl": "https://cdn.nexgate.com/users/ddd/profilepic/thumb.webp"
          }
        ],
        "mentionedShops": [
          {
            "id": "shop-bbb-222",
            "name": "GymGear Store",
            "shopSlug": "gymgear-store",
            "logoUrl": "https://cdn.nexgate.com/shops/gymgear/logo.webp"
          }
        ],
        "collaborators": [
          {
            "id": "eee-555-fff-666-ggg",
            "username": "coachmark",
            "displayName": "Coach Mark",
            "profilePictureUrl": "https://cdn.nexgate.com/users/eee/profilepic/thumb.webp"
          }
        ],
        "currentUserIsCollaborator": false,
        "currentUserIsPendingCollaborator": true,
        "attachments": {
          "products": [
            {
              "id": "prod-aaa-111",
              "name": "Nike Air Max 270",
              "price": 120.00,
              "discountPrice": 89.99,
              "imageUrl": "https://cdn.nexgate.com/products/nike/medium.webp",
              "shopName": "Nike Store",
              "inStock": true
            }
          ],
          "shop": {
            "id": "shop-aaa-111",
            "name": "Nike Store",
            "logoUrl": "https://cdn.nexgate.com/shops/nike/logo.webp"
          },
          "externalLink": null
        },
        "engagement": {
          "viewsCount": 50200,
          "likesCount": 1204,
          "commentsCount": 87,
          "sharesCount": 43,
          "savesCount": 210
        },
        "userInteraction": {
          "hasLiked": false,
          "hasSaved": false,
          "hasShared": false
        },
        "createdAt": "2026-06-20T08:30:00"
      }
    ],
    "nextCursor": "MjAyNi0wNi0yMFQwODozMDowMDozZmE4NWY2NC01NzE3LTQ1NjItYjNmYy0yYzk2M2Y2NmFmYTY=",
    "hasMore": true
  }
}
```

**Success Response Fields**:
| Field | Description |
|-------|-------------|
| `clips` | Array of clip objects for this page |
| `clips[].id` | Media ID — the unique identity of this clip. Use this for view tracking and share links |
| `clips[].postId` | Parent post ID — use this for like, comment, save, and share interactions |
| `clips[].author.id` | Author's account ID |
| `clips[].author.username` | Author's public username |
| `clips[].author.profilePictureUrl` | Medium-size profile picture URL |
| `clips[].author.profilePictureThumbnailUrl` | Thumbnail profile picture URL |
| `clips[].author.profilePictureBlurhash` | Blurhash string for the profile picture placeholder |
| `clips[].author.verified` | Whether the author has a verified badge |
| `clips[].author.following` | Whether the authenticated user follows this author |
| `clips[].caption` | Raw caption text of the parent post |
| `clips[].media.id` | Same as `clips[].id` — the media item ID |
| `clips[].media.duration` | Video duration in seconds |
| `clips[].media.width` | Video width in pixels |
| `clips[].media.height` | Video height in pixels |
| `clips[].media.variants` | Dynamic map of variant keys to fully-assembled URLs. See variants table above |
| `clips[].hashtags` | Array of hashtag strings extracted from the caption (without `#` prefix) |
| `clips[].mentionedUsers` | Array of users @mentioned inline in the caption text |
| `clips[].mentionedUsers[].id` | Mentioned user's account ID |
| `clips[].mentionedUsers[].username` | Mentioned user's public username |
| `clips[].mentionedUsers[].displayName` | Mentioned user's display name (may be `null` if not set) |
| `clips[].mentionedUsers[].profilePictureUrl` | Mentioned user's thumbnail profile picture URL |
| `clips[].mentionedShops` | Array of shops inline-mentioned in the caption via `@ShopName` |
| `clips[].mentionedShops[].id` | Shop ID |
| `clips[].mentionedShops[].name` | Shop display name |
| `clips[].mentionedShops[].shopSlug` | Shop URL slug |
| `clips[].mentionedShops[].logoUrl` | Shop logo URL |
| `clips[].collaborators` | Array of accepted co-authors on this post. Empty array if no collaborators |
| `clips[].collaborators[].id` | Collaborator's account ID |
| `clips[].collaborators[].username` | Collaborator's public username |
| `clips[].collaborators[].displayName` | Collaborator's display name (may be `null` if not set) |
| `clips[].collaborators[].profilePictureUrl` | Collaborator's thumbnail profile picture URL |
| `clips[].currentUserIsCollaborator` | `true` if the authenticated user is an accepted collaborator on this post |
| `clips[].currentUserIsPendingCollaborator` | `true` if the authenticated user has a pending collaboration invite on this post — show an accept/decline prompt |
| `clips[].attachments.products` | Lightweight product cards attached to the post (max display: all attached products) |
| `clips[].attachments.shop` | The first shop attached to the post, or `null` |
| `clips[].attachments.externalLink` | External link with short URL and preview, or `null` |
| `clips[].engagement.viewsCount` | Total unique views for this specific video (per media item, not per post) |
| `clips[].engagement.likesCount` | Total likes on the parent post |
| `clips[].engagement.commentsCount` | Total comments on the parent post |
| `clips[].engagement.sharesCount` | Total shares on the parent post |
| `clips[].engagement.savesCount` | Total saves/bookmarks on the parent post |
| `clips[].userInteraction.hasLiked` | Whether the authenticated user has liked the parent post |
| `clips[].userInteraction.hasSaved` | Whether the authenticated user has saved/bookmarked the parent post |
| `clips[].userInteraction.hasShared` | Whether the authenticated user has shared the parent post |
| `clips[].createdAt` | ISO 8601 timestamp of when the parent post was published |
| `nextCursor` | Opaque cursor string to pass on the next request. `null` when `hasMore` is false |
| `hasMore` | `true` if more clips exist beyond this page, `false` if the feed is exhausted |

**Error Response Examples**:

*Bad cursor (400):*
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Invalid cursor",
  "action_time": "2026-06-22T10:30:45",
  "data": "Invalid cursor"
}
```

*Unauthorized (401):*
```json
{
  "success": false,
  "httpStatus": "UNAUTHORIZED",
  "message": "Token has expired",
  "action_time": "2026-06-22T10:30:45",
  "data": "Token has expired"
}
```

---

## 2. Get Single Clip

**Purpose**: Fetches a single clip by its media ID. Used for deep links, share links, and reopening a clip from a notification or profile tap.

**Endpoint**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `{base_url}/api/v1/e-social/clip/{mediaId}`

**Access Level**: 🔒 Protected (Requires valid JWT)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <jwt_token>` |

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `mediaId` | UUID | Yes | The media ID of the clip | Valid UUID format |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Clip fetched",
  "action_time": "2026-06-22T10:30:45",
  "data": {
    "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "postId": "1a2b3c4d-0000-0000-0000-000000000001",
    "author": {
      "id": "aaa-111-bbb-222-ccc",
      "username": "fitnessguru",
      "profilePictureUrl": "https://cdn.nexgate.com/users/aaa/profilepic/medium.webp",
      "profilePictureThumbnailUrl": "https://cdn.nexgate.com/users/aaa/profilepic/thumb.webp",
      "profilePictureBlurhash": "LKO2?U%2Tw=w]~RBVZRi};RPxuwH",
      "verified": true,
      "following": true
    },
    "caption": "morning workout routine 💪 #fitness #gym",
    "media": {
      "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "duration": 42,
      "width": 1080,
      "height": 1920,
      "variants": {
        "master": "https://cdn.nexgate.com/posts/aaa/media/xyz/hls/master.m3u8",
        "360p_playlist": "https://cdn.nexgate.com/posts/aaa/media/xyz/hls/360p/360p.m3u8",
        "720p_playlist": "https://cdn.nexgate.com/posts/aaa/media/xyz/hls/720p/720p.m3u8",
        "1080p_playlist": "https://cdn.nexgate.com/posts/aaa/media/xyz/hls/1080p/1080p.m3u8",
        "poster": "https://cdn.nexgate.com/posts/aaa/media/xyz/poster.webp",
        "thumb": "https://cdn.nexgate.com/posts/aaa/media/xyz/thumb.webp",
        "og": "https://cdn.nexgate.com/posts/aaa/media/xyz/og.webp",
        "blurhash": "LKO2?U%2Tw=w]~RBVZRi};RPxuwH",
        "lqip": "data:image/webp;base64,UklGRlYAAABXRUJQ...",
        "dominant_color": "#1a1a2e",
        "preview_3s.mp4": "https://cdn.nexgate.com/posts/aaa/media/xyz/preview_3s.mp4",
        "720p_watermarked.mp4": "https://cdn.nexgate.com/posts/aaa/media/xyz/720p_watermarked.mp4"
      }
    },
    "hashtags": ["fitness", "gym"],
    "mentionedUsers": [],
    "mentionedShops": [],
    "collaborators": [
      {
        "id": "eee-555-fff-666-ggg",
        "username": "coachmark",
        "displayName": "Coach Mark",
        "profilePictureUrl": "https://cdn.nexgate.com/users/eee/profilepic/thumb.webp"
      }
    ],
    "currentUserIsCollaborator": true,
    "currentUserIsPendingCollaborator": false,
    "attachments": {
      "products": [],
      "shop": null,
      "externalLink": {
        "shortUrl": "https://nexgate.link/abc123",
        "preview": {
          "title": "Best Workout Plans 2026",
          "imageUrl": "https://external-domain.com/og-image.jpg",
          "domain": "workoutplans.com"
        }
      }
    },
    "engagement": {
      "viewsCount": 50200,
      "likesCount": 1204,
      "commentsCount": 87,
      "sharesCount": 43,
      "savesCount": 210
    },
    "userInteraction": {
      "hasLiked": true,
      "hasSaved": false,
      "hasShared": false
    },
    "createdAt": "2026-06-20T08:30:00"
  }
}
```

**Success Response Fields**: Same field definitions as the feed endpoint above. This returns a single clip object (not wrapped in a feed).

**Error Response Examples**:

*Clip not found (404):*
```json
{
  "success": false,
  "httpStatus": "NOT_FOUND",
  "message": "Clip not found",
  "action_time": "2026-06-22T10:30:45",
  "data": "Clip not found"
}
```

*Media exists but is not a clip (404):*
```json
{
  "success": false,
  "httpStatus": "NOT_FOUND",
  "message": "Media is not a clip",
  "action_time": "2026-06-22T10:30:45",
  "data": "Media is not a clip"
}
```

*Clip exists but is still processing (404):*
```json
{
  "success": false,
  "httpStatus": "NOT_FOUND",
  "message": "Clip is not available",
  "action_time": "2026-06-22T10:30:45",
  "data": "Clip is not available"
}
```

> **Note**: A deleted or unpublished parent post also returns `404 Clip not found` — the API does not distinguish between "media doesn't exist" and "media exists but is hidden" to prevent enumeration.

---

## 3. Record a View

**Purpose**: Records that the authenticated user has watched a clip. The first call increments the public `viewsCount`. Subsequent calls by the same user only increment a personal view counter — the public count is never double-counted. Anonymous users are silently ignored.

**Endpoint**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/api/v1/e-social/clip/{mediaId}/view`

**Access Level**: 🔒 Protected (Requires valid JWT — anonymous calls return `204` but are not recorded)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <jwt_token>` |

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `mediaId` | UUID | Yes | The media ID of the clip being viewed | Valid UUID format |

**Request JSON Sample**:
```json
{
  "watchDurationMs": 38000,
  "source": "FEED"
}
```

**Request Body Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `watchDurationMs` | long | No | How long the user watched in milliseconds | Any positive long value |
| `source` | string | No | Where the user encountered the clip | Recommended: `FEED`, `PROFILE`, `SHARE_LINK` |

> **Note**: The request body is optional. Sending `POST /{mediaId}/view` with no body is valid and records the view.
> `watchDurationMs` and `source` are accepted and stored for future analytics — they do not affect view counting logic today.

**Success Response**:

`204 No Content` — no response body.

**View Counting Logic**:

```
User A watches clip X for the first time:
  ✅ New row written to clip_views (mediaId=X, userId=A, viewCount=1)
  ✅ post_media.views_count incremented by 1
  ✅ posts.views_count incremented by 1

User A watches clip X again:
  ✅ clip_views.view_count incremented (now 2) — personal history only
  ❌ post_media.views_count NOT changed — no inflation
  ❌ posts.views_count NOT changed

Anonymous user (no JWT) watches clip X:
  ✅ Returns 204 silently
  ❌ Nothing recorded
```

**Error Response Examples**:

*Clip not found or deleted (404):*
```json
{
  "success": false,
  "httpStatus": "NOT_FOUND",
  "message": "Clip not found",
  "action_time": "2026-06-22T10:30:45",
  "data": "Clip not found"
}
```

---

## 4. Get User Videos

**Purpose**: Returns all videos (both long-form and short clips) published by a specific user, ordered by most recently published. Used for the videos tab on a user's profile page.

**Endpoint**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `{base_url}/api/v1/e-social/clip/user/{userId}/videos`

**Access Level**: 🔒 Protected (Requires valid JWT)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <jwt_token>` |

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `userId` | UUID | Yes | The account ID of the user whose videos to fetch | Valid UUID format |

**Query Parameters**:
| Parameter | Type | Required | Description | Validation | Default |
|-----------|------|----------|-------------|------------|---------|
| `cursor` | string | No | Opaque pagination cursor from the previous response | Valid base64 cursor string | `null` (first page) |
| `limit` | integer | No | Number of videos to return per page | Min: 1, Max: 20 | `12` |

> **Difference from `/clip/feed`**: This endpoint does NOT filter by `shortClip = true`. It returns all processed videos from the user regardless of length, making it suitable for the profile videos grid.

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "User videos fetched",
  "action_time": "2026-06-22T10:30:45",
  "data": {
    "clips": [
      {
        "id": "7cb91a00-0000-0000-0000-000000000001",
        "postId": "8dc02b11-0000-0000-0000-000000000002",
        "author": {
          "id": "aaa-111-bbb-222-ccc",
          "username": "fitnessguru",
          "profilePictureUrl": "https://cdn.nexgate.com/users/aaa/profilepic/medium.webp",
          "profilePictureThumbnailUrl": "https://cdn.nexgate.com/users/aaa/profilepic/thumb.webp",
          "profilePictureBlurhash": "LKO2?U%2Tw=w]~RBVZRi};RPxuwH",
          "verified": true,
          "following": true
        },
        "caption": "Full 30-minute chest day 🔥 #gym #workout",
        "media": {
          "id": "7cb91a00-0000-0000-0000-000000000001",
          "duration": 1842,
          "width": 1920,
          "height": 1080,
          "variants": {
            "master": "https://cdn.nexgate.com/posts/aaa/media/abc/hls/master.m3u8",
            "360p_playlist": "https://cdn.nexgate.com/posts/aaa/media/abc/hls/360p/360p.m3u8",
            "720p_playlist": "https://cdn.nexgate.com/posts/aaa/media/abc/hls/720p/720p.m3u8",
            "1080p_playlist": "https://cdn.nexgate.com/posts/aaa/media/abc/hls/1080p/1080p.m3u8",
            "poster": "https://cdn.nexgate.com/posts/aaa/media/abc/poster.webp",
            "thumb": "https://cdn.nexgate.com/posts/aaa/media/abc/thumb.webp",
            "blurhash": "LKO2?U%2Tw=w]~RBVZRi};RPxuwH",
            "lqip": "data:image/webp;base64,UklGRlYAAABXRUJQ...",
            "dominant_color": "#0d0d1a"
          }
        },
        "hashtags": ["gym", "workout"],
        "mentionedUsers": [],
        "mentionedShops": [],
        "collaborators": [],
        "currentUserIsCollaborator": false,
        "currentUserIsPendingCollaborator": false,
        "attachments": {
          "products": [],
          "shop": null,
          "externalLink": null
        },
        "engagement": {
          "viewsCount": 8900,
          "likesCount": 320,
          "commentsCount": 14,
          "sharesCount": 8,
          "savesCount": 45
        },
        "userInteraction": {
          "hasLiked": false,
          "hasSaved": false,
          "hasShared": false
        },
        "createdAt": "2026-06-18T14:00:00"
      }
    ],
    "nextCursor": "MjAyNi0wNi0xOFQxNDowMDowMDo3Y2I5MWEwMC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMDAwMDE=",
    "hasMore": false
  }
}
```

**Success Response Fields**: Same as the feed response. Note that `shortClip` is not part of the response — long videos and short clips look identical in this response. The frontend can distinguish them by `media.duration` if needed (short clips are under 180 seconds).

**Error Response Examples**:

*Unauthorized (401):*
```json
{
  "success": false,
  "httpStatus": "UNAUTHORIZED",
  "message": "Token has expired",
  "action_time": "2026-06-22T10:30:45",
  "data": "Token has expired"
}
```

---

## How Clip Interactions Work

Clips share the social layer of their parent post. The clip response always includes `postId` so the frontend knows which ID to use for each action.

| Action | Endpoint | ID to use |
|---|---|---|
| Like a clip | `POST /api/v1/e-social/posts/{postId}/like` | `postId` from clip response |
| Unlike a clip | `DELETE /api/v1/e-social/posts/{postId}/like` | `postId` from clip response |
| Comment on a clip | `POST /api/v1/e-social/posts/{postId}/comments` | `postId` from clip response |
| Save a clip | `POST /api/v1/e-social/posts/{postId}/bookmark` | `postId` from clip response |
| Share a clip | `POST /api/v1/e-social/posts/{postId}/share` | `postId` from clip response |
| Record a view | `POST /api/v1/e-social/clip/{mediaId}/view` | `id` (media ID) from clip response |
| Deep link to clip | `GET /api/v1/e-social/clip/{mediaId}` | `id` (media ID) from clip response |
| Accept collaboration invite | `POST /api/v1/e-social/posts/{postId}/collaboration/accept` | `postId` from clip response |
| Decline collaboration invite | `POST /api/v1/e-social/posts/{postId}/collaboration/decline` | `postId` from clip response |

> **Collaboration flow**: when `currentUserIsPendingCollaborator` is `true`, show the user an accept/decline prompt. Use the `postId` from the clip response with the existing post collaboration endpoints above. Once accepted, `currentUserIsCollaborator` becomes `true` and the user appears in the `collaborators` array.

---

## Quick Reference

### Endpoint Summary

| Method | Path | Auth | Purpose |
|---|---|---|---|
| <span style="background-color: #28a745; color: white; padding: 2px 6px; border-radius: 3px; font-family: monospace; font-size: 11px;">GET</span> | `/api/v1/e-social/clip/feed` | 🔒 JWT | Scroll feed — paginated clips |
| <span style="background-color: #28a745; color: white; padding: 2px 6px; border-radius: 3px; font-family: monospace; font-size: 11px;">GET</span> | `/api/v1/e-social/clip/{mediaId}` | 🔒 JWT | Single clip by media ID |
| <span style="background-color: #007bff; color: white; padding: 2px 6px; border-radius: 3px; font-family: monospace; font-size: 11px;">POST</span> | `/api/v1/e-social/clip/{mediaId}/view` | 🔒 JWT | Record a view |
| <span style="background-color: #28a745; color: white; padding: 2px 6px; border-radius: 3px; font-family: monospace; font-size: 11px;">GET</span> | `/api/v1/e-social/clip/user/{userId}/videos` | 🔒 JWT | All videos from a user |

### Key ID Reference

| Use Case | Which ID | Field Name |
|---|---|---|
| Share link, deep link, view tracking | Media ID | `clip.id` |
| Like, comment, save, share | Post ID | `clip.postId` |

### Recommended Playback Priority

```
1. variants["master"]        → HLS adaptive bitrate (best quality, adapts to connection)
2. variants["720p_playlist"] → HLS fixed 720p fallback
3. variants["360p_playlist"] → HLS fixed 360p low bandwidth fallback
```

### Recommended Loading Placeholder Priority

```
1. variants["lqip"]          → Show immediately (inline base64, no network request)
2. variants["blurhash"]      → Render as CSS blur using a blurhash library
3. variants["dominant_color"]→ Solid color fallback
```