---
title: "YouTube Search API"
description: "Search live YouTube for videos, channels, playlists, and shorts. Results come straight from YouTube at request time — nothing is read from the NexLev catalog."
source: "https://dashboard.nexlev.io/docs/api/youtube-search"
---

# YouTube Search API

Search **live YouTube** for videos, channels, playlists, and shorts. Results come straight from YouTube at request time — nothing is read from the NexLev catalog.

**Base URL**: `https://prod.dashboard.nexlev.io` — see [Authentication](/docs/api/authentication). This operation costs 🥞 1 quota per request — see [Rate Limits & Quota](/docs/api/rate-limits).

Requests are served with automatic failover across upstream data providers. The `source` field in every response reports whether the primary or the fallback path answered; the payload shape is identical either way.

> **Info**
>
> **Not to be confused with the catalog search.** `GET
> /api/external/videos/video-search` searches NexLev's own indexed video catalog
> and supports catalog-only filters such as outlier score, subscriber count, and
> RPM. Use that one for research over curated data; use **this** one when you
> need what YouTube is returning right now.

## Operations Quick Reference

| Operation      | Method & Path                      | Quota Cost |
| -------------- | ---------------------------------- | ---------- |
| Search YouTube | `GET /api/external/youtube/search` | 🥞 1       |

## Rate Limit

> **Warning**
>
> **20 requests per minute** per API key on this endpoint, on top of your plan
> quota. Exceeding either returns `429 Too Many Requests` — see [Rate Limits &
> Quota](/docs/api/rate-limits).

## Search YouTube

```http
GET /api/external/youtube/search
```

**Description**: Run a YouTube search and return the videos, channels, playlists, and shorts YouTube currently ranks for that query, with optional type, duration, recency, feature, and region filters.

> **Warning**
>
> **🥞 Quota Cost: 1 quota per request**
>
> Each call to this endpoint deducts **1 quota** from your account balance. Paginated requests each count separately. Monitor your usage at [Manage API Key](https://dashboard.nexlev.io/nexlev-api/manage-api-key) to avoid interruptions.
>

**When to use this operation:**

- Checking what YouTube ranks for a keyword right now
- Keyword and title research against live search results
- Finding channels or playlists by name when you do not have a channel ID
- Sampling shorts for a topic without going channel by channel

### Query Parameters

Parameter names are **camelCase**. Unknown parameters are **rejected with a `400`** rather than silently ignored — so a typo or a snake_case name (`sort_by`, `upload_date`) fails loudly instead of returning unfiltered results.

> **Warning**
>
> This endpoint's naming differs from the older `channels/*` routes, which use
> `id`, `sort_by`, and `forUsername`. Do not copy parameter names between them.

| Parameter    | Type                                                                | Required | Default                       | Description                                                                                               |
| ------------ | ------------------------------------------------------------------- | -------- | ----------------------------- | --------------------------------------------------------------------------------------------------------- |
| `query`      | string                                                              | **Yes**  | —                             | Search terms, e.g. `mkbhd`.                                                                               |
| `type`       | `video` \| `channel` \| `playlist` \| `shorts` \| `movie` \| `show` | No       | —                             | Restrict to one content type. **Omit to get a mixed result set** — check the `type` field on each item.   |
| `sortBy`     | `relevance` \| `popularity` \| `rating` \| `date` \| `views`        | No       | `relevance` (YouTube default) | Result ordering.                                                                                          |
| `duration`   | `short` \| `medium` \| `long`                                       | No       | —                             | `short` under 4 min, `medium` 4–20 min, `long` over 20 min.                                               |
| `uploadDate` | `hour` \| `today` \| `week` \| `month` \| `year`                    | No       | —                             | Restrict by upload recency.                                                                               |
| `features`   | string (comma-separated)                                            | No       | —                             | Any of `HD`, `subtitles`, `CCommons`, `3D`, `Live`, `4K`, `360`, `HDR`, `VR180`. Example: `HD,subtitles`. |
| `geo`        | string                                                              | No       | —                             | ISO 3166-2 country code, e.g. `US`, `GB`, `IN`. Changes which regional results YouTube returns.           |
| `lang`       | string                                                              | No       | —                             | Locale, e.g. `en`, `gb`, `hi`.                                                                            |
| `token`      | string                                                              | No       | —                             | Pagination token. Pass the `continuation` value from the previous response. Omit for the first page.      |

### Example URLs

```
https://prod.dashboard.nexlev.io/api/external/youtube/search?query=mkbhd
https://prod.dashboard.nexlev.io/api/external/youtube/search?query=mkbhd&type=video&sortBy=views
```

**cURL Example**:

```bash
curl "https://prod.dashboard.nexlev.io/api/external/youtube/search?query=mkbhd&type=video&sortBy=views" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**Status Code**: `200 OK`

**Response Body**:

```json
{
  "query": "mkbhd",
  "estimatedResults": "360582",
  "continuation": "EpoDEgVta2JoZBqQA0VnSVFBVWdV...",
  "resultCount": 20,
  "source": "primary",
  "results": [
    {
      "type": "video",
      "videoId": "_xjxwl1zLMc",
      "title": "Framework 13 Pro: The Modular Laptop is Real!",
      "channelTitle": "Marques Brownlee",
      "channelId": "UCBJycsmduvYEL83R_U4JriQ",
      "channelHandle": "@mkbhd",
      "description": "The modular Macbook Pro for Linux users. Color me impressed...",
      "viewCount": "2643412",
      "publishedTime": "3 days ago",
      "duration": "12:49",
      "isLive": false,
      "badges": ["New", "4K"],
      "thumbnail": [
        {
          "url": "https://i.ytimg.com/vi/_xjxwl1zLMc/hq720.jpg?...",
          "width": 720,
          "height": 404
        },
        {
          "url": "https://i.ytimg.com/vi/_xjxwl1zLMc/hq720.jpg?...",
          "width": 360,
          "height": 202
        }
      ]
    }
  ],
  "filters": {
    "type": "video",
    "sortBy": "views"
  }
}
```

### Envelope Fields

- `query` - The query that was executed
- `estimatedResults` - YouTube's own estimate of total matches, as a numeric string. **May be absent** on some responses — do not assume it is present
- `continuation` - Token for the next page. `null` means there are no further pages
- `resultCount` - Number of items in `results` for this page
- `source` - `"primary"` or `"fallback"`; which leg of the failover chain served the request. Informational only
- `results` - The result items. **The shape varies by `type`** — see below
- `filters` - Echo of the filters that were applied. Only `type`, `sortBy`, `duration`, `uploadDate`, and `geo` are echoed — `features` and `lang` are not. Filters you did not pass are **omitted**, not `null`, so this is `{}` for an unfiltered query

## Result Item Shapes

Every item carries a `type` discriminator. **Branch on it** — the available fields differ significantly. Pick a type below for its fields and a sample payload.

### Video

#### Video Item

The richest shape — full metadata for a long-form video or a live stream.

```json
{
  "type": "video",
  "videoId": "_xjxwl1zLMc",
  "title": "Framework 13 Pro: The Modular Laptop is Real!",
  "channelTitle": "Marques Brownlee",
  "channelId": "UCBJycsmduvYEL83R_U4JriQ",
  "channelHandle": "@mkbhd",
  "description": "The modular Macbook Pro for Linux users. Color me impressed...",
  "viewCount": "2643412",
  "publishedTime": "3 days ago",
  "duration": "12:49",
  "isLive": false,
  "badges": ["New", "4K"],
  "thumbnail": [
    {
      "url": "https://i.ytimg.com/vi/_xjxwl1zLMc/hq720.jpg?...",
      "width": 720,
      "height": 404
    }
  ]
}
```

**Response Fields**:

- `videoId` - YouTube video ID
- `title` - Video title
- `channelTitle` / `channelId` / `channelHandle` - Uploading channel
- `description` - Truncated search-snippet description, not the full video description
- `viewCount` - View count as a **numeric string**
- `publishedTime` - Relative human string, e.g. `"3 days ago"`
- `duration` - Display string, e.g. `"12:49"`
- `isLive` - `true` for an ongoing live stream
- `badges` - YouTube badges such as `New`, `4K`, `CC`
- `thumbnail` - Array of thumbnail objects with `url`, `width`, `height`

### Shorts

#### Shorts Item

Sparse. YouTube nests shorts inside a shelf object; this endpoint lifts them out so every shorts item is an individual, linkable result.

```json
{
  "type": "shorts",
  "videoId": "YHqpKFaFbyc",
  "title": "Tomahawk Steak Frites!🥩 @ChefRush",
  "viewCount": 29000000,
  "thumbnail": []
}
```

**Response Fields**:

- `videoId` - YouTube video ID
- `title` - Shorts title
- `viewCount` - View count as a **number** (not a string, unlike `video` items)
- `thumbnail` - Frequently an empty array — fall back to `https://i.ytimg.com/vi/{videoId}/hqdefault.jpg`

> **Warning**
>
> Shorts items carry **no channel fields**. If you need the uploading channel,
> call [Video Details](/docs/api/video-content#video-details) with the
> `videoId`.

### Channel

#### Channel Item

```json
{
  "type": "channel",
  "title": "Marques Brownlee",
  "channelTitle": "Marques Brownlee",
  "channelId": "UCBJycsmduvYEL83R_U4JriQ",
  "channelHandle": "@mkbhd",
  "description": "MKBHD: Quality Tech Videos | YouTuber | Geek | ...",
  "thumbnail": [{ "url": "//yt3.ggpht.com/...", "width": 176, "height": 176 }]
}
```

**Response Fields**:

- `channelId` - Channel ID, usable directly with [Channel Content](/docs/api/channel-content) and [Channel Analytics](/docs/api/channel-analytics)
- `title` / `channelTitle` - Channel name (both present, same value)
- `channelHandle` - Handle, e.g. `@mkbhd`
- `description` - Channel description snippet
- `thumbnail` - Avatar images

> **Warning**
>
> Channel thumbnails may be **protocol-relative** (`//yt3.ggpht.com/...`).
> Prefix with `https:` before use.

There is **no subscriber count** in the search payload. Call [Get About](/docs/api/channel-content#about) with the `channelId` for full channel stats.

### Playlist

#### Playlist Item

```json
{
  "type": "playlist",
  "title": "Tech Reviews 2025",
  "videoId": "_xjxwl1zLMc",
  "channelTitle": "Marques Brownlee",
  "channelId": "UCBJycsmduvYEL83R_U4JriQ",
  "channelHandle": "@mkbhd",
  "thumbnail": [
    {
      "url": "https://i.ytimg.com/vi/_xjxwl1zLMc/hqdefault.jpg",
      "width": 336,
      "height": 188
    }
  ]
}
```

**Response Fields**:

- `title` - Playlist title
- `videoId` - The **first video in the playlist**, not a playlist ID
- `channelTitle` / `channelId` / `channelHandle` - Owning channel
- `thumbnail` - Playlist cover images

To enumerate a channel's playlists properly, use [Get Playlists](/docs/api/channel-content#playlists).

## Integration Notes

These are all consequences of what YouTube returns upstream — worth handling defensively:

1. **`viewCount` is not consistently typed.** It is a numeric **string** on `video` items (`"2643412"`) and a **number** on `shorts` items (`29000000`). Coerce before doing arithmetic.
2. **`publishedTime` is a relative human string**, not a timestamp — `"3 days ago"`, `"1 year ago"`. There is no absolute date in the search payload; call [Video Details](/docs/api/video-content#video-details) if you need one.
3. **`duration` is a display string** (`"12:49"`), not seconds.
4. **Shorts are flattened** out of their shelf, and their thumbnail array is frequently empty — fall back to `https://i.ytimg.com/vi/{videoId}/hqdefault.jpg`.
5. **Channel thumbnails may be protocol-relative** (`//yt3.ggpht.com/...`). Prefix with `https:` before use.
6. **Mixed results by default.** Without `type`, a single page can contain videos, channels, playlists, and shorts together, and `resultCount` counts them all.
7. **Result counts per page are not fixed** and are not controllable — there is no `limit` parameter. Observed pages range from ~20 to 80+ items, depending on what YouTube returns and how many shorts got unpacked from their shelf. Paginate with `token`; never assume a page size.
8. **`type=movie` and `type=show`** are accepted and do return results, but items are tagged with the standard types above — `movie` comes back as `video` items, and `show` returns a mix of `video`, `shorts`, and `playlist`. There is no `movie` or `show` item type.

## Pagination

When more results are available, the response returns a `continuation` token. Pass it back as `token` to fetch the next page.

**How to use continuation tokens:**

1. Make an initial request without `token` to get the first page
2. Check the `continuation` field in the response:
   - If it contains a token, more results are available
   - If it is `null`, you have reached the last page
3. To fetch the next page, repeat the request with `token` set to that value — and **resend the same `query` and all the same filters**
4. Repeat until `continuation` is `null`

> **Warning**
>
> Tokens are opaque and tied to the original query. Changing `query`, `type`,
> `sortBy`, or any other filter alongside a token invalidates it — start over
> instead.

**Example pagination workflow:**

```bash
# page 1
curl "https://prod.dashboard.nexlev.io/api/external/youtube/search?query=cooking%20tutorials&type=video" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```json
Response: {
  "continuation": "EpoDEgVta2JoZBqQA0VnSVFBVWdV...",
  "resultCount": 20,
  "results": [
    /* Array of result items */
  ]
}
```

```bash
# page 2 — feed `continuation` back in as `token`
curl "https://prod.dashboard.nexlev.io/api/external/youtube/search?query=cooking%20tutorials&type=video&token=EpoDEgVta2JoZBqQA0VnSVFBVWdV..." \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```json
Response: {
  "continuation": null,  // null means no more results
  "resultCount": 18,
  "results": [
    /* Array of remaining result items */
  ]
}
```

> **Info**
>
> Unlike the `channels/*` endpoints, which signal "no more pages" with an
> **empty string**, this endpoint uses **`null`**. Check for both if you share
> pagination code across endpoints.

## Error Responses

### Bad Request

**Status Code**: `400 Bad Request`

**Description**: Validation failure — a missing `query`, an out-of-enum value, or an **unrecognized parameter name**. The response `message` lists exactly what was wrong.

```json
{
  "success": false,
  "statusCode": 400,
  "message": [
    "property sort_by should not exist",
    "property upload_date should not exist"
  ],
  "error": "Bad Request",
  "path": "/api/external/youtube/search?query=x&sort_by=date&upload_date=week"
}
```

### Unauthorized

**Status Code**: `401 Unauthorized`

**Description**: Authentication credentials are missing or invalid.

```json
{
  "error": {
    "code": 401,
    "message": "Unauthorized",
    "details": "Invalid API key or credentials not provided. Create your API key at https://dashboard.nexlev.io/nexlev-api/create-api-key"
  }
}
```

### Rate Limit

**Status Code**: `429 Too Many Requests`

**Description**: You exceeded either the per-minute rate limit (20 requests per minute on this endpoint) or your plan quota.

```json
{
  "error": {
    "code": 429,
    "message": "Rate limit exceeded",
    "details": "You have exceeded your API quota. Please try again later.",
    "retryAfter": 3600
  }
}
```

### Upstream

**Status Code**: `502 Bad Gateway`

**Description**: The upstream YouTube data provider is unavailable — both the primary and the fallback path failed. Safe to retry, ideally with exponential backoff.

```json
{
  "error": {
    "code": 502,
    "message": "Upstream provider unavailable",
    "details": "The YouTube data provider did not respond. Please retry."
  }
}
```

## More Examples

```bash
# Most-viewed videos for a term
curl "https://prod.dashboard.nexlev.io/api/external/youtube/search?query=mkbhd&type=video&sortBy=views" \
  -H "Authorization: Bearer YOUR_API_KEY"

# Channels only
curl "https://prod.dashboard.nexlev.io/api/external/youtube/search?query=mkbhd&type=channel" \
  -H "Authorization: Bearer YOUR_API_KEY"

# Shorts, US region
curl "https://prod.dashboard.nexlev.io/api/external/youtube/search?query=cooking&type=shorts&geo=US&lang=en" \
  -H "Authorization: Bearer YOUR_API_KEY"

# Short, recent, HD videos with subtitles
curl "https://prod.dashboard.nexlev.io/api/external/youtube/search?query=news&type=video&duration=short&uploadDate=week&features=HD,subtitles" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

## Related Endpoints

| Endpoint                                               | Data source    | Use when                                                                |
| ------------------------------------------------------ | -------------- | ----------------------------------------------------------------------- |
| `GET /api/external/youtube/search`                     | Live YouTube   | You need current YouTube search results.                                |
| `GET /api/external/videos/video-search`                | NexLev catalog | You need catalog filters — outlier score, subscriber count, RPM, dates. |
| [Video Details](/docs/api/video-content#video-details) | Live YouTube   | Full metadata for one video, including absolute publish date.           |
| [Get About](/docs/api/channel-content#about)           | Live YouTube   | Channel stats for one channel (note: takes `id`, not `channelId`).      |
| [Get Videos](/docs/api/channel-content#videos)         | Live YouTube   | A specific channel's uploads (takes `id` and `sort_by`).                |
