Skip to main content

Tapline — Official Python Client

tapline

PyPI Python

Tapline is a Python YouTube scraper and YouTube transcript extractor for public data. You can scrape YouTube transcripts, search videos, and collect structured data for videos, channels, and playlists. Extract YouTube comments and replies one page at a time. Use the pagination helper to scrape YouTube comments beyond the first page.

Tapline requires Python 3.10 or newer and an API key. Get your key on the Tapline YouTube page. Every new account starts with 500 free credits, and you do not need a card.

Install

pip install tapline

Set your API key as an environment variable:

export TAPLINE_API_KEY="your-api-key"

The examples below read the key from that variable automatically.

You can also pass the key when you create the client:

from tapline import SyncTaplineClient

tapline = SyncTaplineClient(api_key="your-api-key")

SyncTaplineClient blocks. TaplineClient is its awaitable twin, for an async application:

from tapline import TaplineClient

tapline = TaplineClient(api_key="your-api-key")

Client methods

Method What you get
search(query=...) Search results
metadata(video) Title, counts, dates, chapters, thumbnails
subtitles(video) A transcript document
subtitle_tracks(video) The languages a video has captions in
comments(video) One page of comment threads
comment_replies(video, comment_id, cursor=...) One page of replies
channel(channel) A channel's profile and subscriber count
channel_videos(channel) One page of a channel's uploads
playlist(playlist_id) A playlist and its videos
heatmap(video) Which parts of a video get replayed
formats(video) The streams YouTube offers for a video

Every method is on both the sync and async clients, and takes an optional timeout that overrides the client's own for that one call.

Get a transcript

Use SyncTaplineClient for a regular Python script:

from tapline import SyncTaplineClient

with SyncTaplineClient() as tapline:
    subtitles = tapline.youtube.subtitles(
        "https://www.youtube.com/watch?v=jNQXAC9IVRw",
        subtitle_format="txt",
    )

    print(subtitles.transcript)

The default language is English. Tapline accepts BCP 47 language tags such as en, pt-BR, es-419, and zh-Hans:

subtitles = tapline.youtube.subtitles(
    "jNQXAC9IVRw",
    language="pt-BR",
    subtitle_format="txt",
)

Tapline can also return srt, vtt, ttml, or json3 transcripts.

The default source="any" prefers a human-authored track and falls back to a machine-generated one. Pass source="manual" or source="auto" to insist on one or the other.

List subtitle languages

If you do not know which languages a video has, check its subtitle tracks:

from tapline import SyncTaplineClient

with SyncTaplineClient() as tapline:
    tracks = tapline.youtube.subtitle_tracks("jNQXAC9IVRw")

    for track in tracks.manual + tracks.auto:
        print(track.language, track.language_name)

Each track's language and formats feed straight back into subtitles.

Get every transcript from a channel

pages walks a channel's uploads for you, so a whole channel is one loop over another — see walking pages:

from tapline import NotFoundError, SyncTaplineClient

with SyncTaplineClient() as tapline:
    pages = tapline.youtube.pages(
        lambda cursor: tapline.youtube.channel_videos("@Computerphile2", cursor=cursor)
    )

    for page in pages:
        for video in page.videos:
            try:
                subtitles = tapline.youtube.subtitles(video.video_id, subtitle_format="txt")
            except NotFoundError:
                continue

            print(video.title)
            print(subtitles.transcript)

A video with no English track raises NotFoundError, which is why the loop skips it rather than stopping. Pass content_type="shorts" or content_type="streams" to walk those tabs instead.

Look up a video

from tapline import SyncTaplineClient

with SyncTaplineClient() as tapline:
    video = tapline.youtube.metadata("https://youtu.be/dQw4w9WgXcQ")

    print(video.title)
    print(video.view_count)
    print(video.duration)

The full record also carries the description, tags, chapters, thumbnails, upload date, channel details, and live status. Ask for a subset with fields when you only need part of it:

video = tapline.youtube.metadata("dQw4w9WgXcQ", fields=["title", "view_count"])

Under a projection the keys you did not ask for are absent rather than null, so use video.model_fields_set to tell them apart from fields the server sent as None. video_id always comes back.

Search YouTube

from tapline import SyncTaplineClient

with SyncTaplineClient() as tapline:
    results = tapline.youtube.search(
        query="learn python",
        limit=5,
    )

    for result in results.results:
        print(result.title)

Searches can be narrowed by upload date, duration, result type, country, and features such as subtitles or 4K video:

results = tapline.youtube.search(
    query="learn python",
    sort="view_count",
    upload_date="this_year",
    search_type="video",
    duration="over_20_min",
    features=["hd", "subtitles"],
    country="BR",
)

Get comments

from tapline import SyncTaplineClient

with SyncTaplineClient() as tapline:
    comments = tapline.youtube.comments("dQw4w9WgXcQ", sort="new")

    for thread in comments.threads:
        print(thread.comment.author, thread.comment.text, thread.comment.like_count)

Comments come back one page at a time, twenty threads at most. Sort by top (the default) or new; pinned threads surface first either way. To read every page, see walking pages.

Get replies to a comment

Each thread carries a replies_cursor, which is where its replies start. It is None when the comment has no replies:

from tapline import SyncTaplineClient

with SyncTaplineClient() as tapline:
    comments = tapline.youtube.comments("dQw4w9WgXcQ")
    thread = comments.threads[0]

    if thread.replies_cursor:
        replies = tapline.youtube.comment_replies(
            "dQw4w9WgXcQ",
            thread.comment.comment_id,
            cursor=thread.replies_cursor,
        )

        for reply in replies.replies:
            print(reply.author, reply.text)

List a channel's videos

from tapline import SyncTaplineClient

with SyncTaplineClient() as tapline:
    uploads = tapline.youtube.channel_videos("@Computerphile2")

    for video in uploads.videos:
        print(video.title, video.view_count, video.url)

Pass content_type="shorts" or content_type="streams" for the other two tabs. Like comments, uploads arrive one page at a time.

Look up a channel

from tapline import SyncTaplineClient

with SyncTaplineClient() as tapline:
    channel = tapline.youtube.channel("@Computerphile2")

    print(channel.channel)
    print(channel.handle)
    print(channel.channel_follower_count)
    print(channel.description)

Get a playlist

Playlists are looked up by ID — the list= parameter of a playlist URL, not the URL itself:

from tapline import SyncTaplineClient

with SyncTaplineClient() as tapline:
    playlist = tapline.youtube.playlist("PLbpi6ZahtOH6Blw3RGYpWkSByi_T7Rygb", limit=100)

    print(playlist.title, playlist.total_count)

    for entry in playlist.entries:
        print(entry.title, entry.url)

limit caps how many entries come back, up to 100. total_count reports how many the playlist actually holds.

Find the most replayed moments

YouTube's heatmap scores a hundred equal slices of the timeline by replay intensity, from 0.0 to 1.0:

from tapline import SyncTaplineClient

with SyncTaplineClient() as tapline:
    heatmap = tapline.youtube.heatmap("dQw4w9WgXcQ")

    if heatmap.heatmap:
        peak = max(heatmap.heatmap, key=lambda point: point.value)
        print(f"Most replayed at {peak.start_time:.0f}s")

        for point in heatmap.heatmap:
            print(point.start_time, point.end_time, point.value)

heatmap is None for the many videos YouTube publishes no heatmap for, which is not an error. The same points also arrive on metadata under heatmap.

List stream formats

from tapline import SyncTaplineClient

with SyncTaplineClient() as tapline:
    formats = tapline.youtube.formats("dQw4w9WgXcQ")

    for stream in formats.formats:
        print(stream.format_id, stream.resolution, stream.ext, stream.filesize)

Each url is signed by YouTube and expires within hours, so fetch it when you are ready to use it. Storyboard formats are left out.

Airbnb

The same client carries the Airbnb endpoints under tapline.airbnb:

Method What you get
airbnb.search_locations(query=...) Place suggestions with a place_id to search by
airbnb.search(...) Listings in an area, by place_id, query, centre and radius_km, or a bounding box
airbnb.get_price(room_id=..., check_in=..., check_out=...) Total and nightly price for a stay
airbnb.get_calendar(room_id=..., month=..., year=...) Availability by day
airbnb.get_reviews(room_id=...) Reviews, newest first
airbnb.get_details(room_id=...) The listing page, parsed
from tapline import SyncTaplineClient

with SyncTaplineClient() as tapline:
    places = tapline.airbnb.search_locations(query="Lisbon")
    results = tapline.airbnb.search(place_id=places.suggestions[0].place_id, adults=2)
    for listing in results.listings:
        print(listing.room_id, listing.name, listing.price.formatted)

Every Airbnb type lives in tapline.airbnb. Enums that travel into a request, such as AmenityFilter, are closed and also accept their string value; enums that arrive on a response are open, so a value Airbnb adds tomorrow parses today.

GMGN

tapline.gmgn covers GMGN's on-chain token data across Solana, Base, BSC, Ethereum and Tron: 61 methods spanning trending leaderboards, token security and holder breakdowns, wallet PnL, launchpad fees and candles.

Method What you get
gmgn.search(chain, q=...) Tokens and wallets matching a name, symbol or address
gmgn.trending_tokens(requests=[...]) Trending leaderboards for several chains in one call
gmgn.multi_token_full_info(...) Pool, security, rug and social data for a batch of tokens
gmgn.token_candles(...) Price OHLCV candles for a token
gmgn.holders(...) A token's holders, with balances and shares
gmgn.security(...) Honeypot, tax and ownership checks
from tapline import SyncTaplineClient

with SyncTaplineClient() as tapline:
    hits = tapline.gmgn.search("sol", q="bonk")
    print(hits)

GeckoTerminal

tapline.geckoterminal covers GeckoTerminal's DEX data: 28 methods across pools, swaps, tokens, categories and trending.

Method What you get
geckoterminal.global_stats() Total 24h volume, pool and network counts, fear-and-greed index
geckoterminal.networks() Every chain indexed, with its identifier and native token
geckoterminal.network_latest_pools(network) Newly created pools on one chain, newest first
geckoterminal.pools(...) Screen pools across every chain by liquidity, volume, age, holders and taxes
geckoterminal.pool(network, address) Full pool detail: price, reserves, GT score, security, tokens
geckoterminal.pool_swaps(...) Individual swaps in a pool, by cursor
geckoterminal.token_top_holders(...) Largest holders, with balance and share of supply
geckoterminal.trends() Top gainers, newest pools and hottest pairs
from tapline import SyncTaplineClient

with SyncTaplineClient() as tapline:
    stats = tapline.geckoterminal.global_stats()
    fresh = tapline.geckoterminal.network_latest_pools("solana")
    for pool in fresh.data[:5]:
        print(pool.attributes.address, pool.attributes.name)

Every GMGN and GeckoTerminal type lives in tapline.gmgn and tapline.geckoterminal. Both modules are generated from the published OpenAPI schema, and each call costs 2 credits.

Walk every page

comments, comment_replies, and channel_videos return one page per call. pages walks one of them to its end, requesting the next page only as you ask for it:

from tapline import SyncTaplineClient

with SyncTaplineClient() as tapline:
    pages = tapline.youtube.pages(
        lambda cursor: tapline.youtube.comments("dQw4w9WgXcQ", cursor=cursor)
    )

    for page in pages:
        for thread in page.threads:
            print(thread.comment.text)

Each page is its own request, so a walk is billed per page rather than once. Replies have no first page without a cursor, so start that walk from a thread's replies_cursor:

reply_pages = tapline.youtube.pages(
    lambda cursor: tapline.youtube.comment_replies(video_id, comment_id, cursor=cursor),
    cursor=thread.replies_cursor,
)

A walk ends when a page's pagination.next_cursor is None. That page's pagination.completion says why: exhausted when YouTube ran out, or depth_limit when Tapline hit its paging cap — sorting comments by top stops at roughly 1,200 of them.

Use the async client

For an async application, use TaplineClient and await:

import asyncio

from tapline import TaplineClient


async def main() -> None:
    async with TaplineClient() as tapline:
        video = await tapline.youtube.metadata("dQw4w9WgXcQ")
        print(video.title)


asyncio.run(main())

The sync and async clients have the same YouTube methods. pages becomes an async generator, driven with async for, and calls can be in flight together:

async with TaplineClient() as tapline:
    video, comments = await asyncio.gather(
        tapline.youtube.metadata("dQw4w9WgXcQ"),
        tapline.youtube.comments("dQw4w9WgXcQ"),
    )

IDs, handles, and URLs

You can pass a video ID or a common YouTube video URL wherever a video is needed:

"dQw4w9WgXcQ"

"https://youtu.be/dQw4w9WgXcQ"
"https://www.youtube.com/watch?v=dQw4w9WgXcQ"

For channels, you can use a channel ID, handle, or URL:

"UCXuqSBlHAE6Xw-yeJA0Tunw"

"@LinusTechTips"
"https://www.youtube.com/@LinusTechTips"

Enums and responses

Anywhere a method takes an enum it also takes that enum's value as a plain string, so these are the same call:

from tapline.youtube import SearchSort

tapline.youtube.search(query="learn python", sort=SearchSort.VIEW_COUNT)
tapline.youtube.search(query="learn python", sort="view_count")

Every response is a pydantic model, so model_dump() gives you a dict and model_dump_json() gives you JSON. Fields a newer Tapline release adds are kept rather than dropped — read them from model_extra.

Handle errors

from tapline import NotFoundError, PermissionDeniedError, SyncTaplineClient

with SyncTaplineClient() as tapline:
    try:
        video = tapline.youtube.metadata("dQw4w9WgXcQ")
    except NotFoundError:
        print("no such video")
    except PermissionDeniedError as error:
        print("not public:", error.message, error.request_id)

Everything the library raises is a TaplineError. A failed request is an APIError; a non-2xx answer is an APIStatusError carrying status_code, code, message, request_id, and details. The ones you are most likely to catch by name:

Exception When
NotFoundError No such video, channel, playlist, or subtitle track
PermissionDeniedError Private, age-gated, members-only, or region-blocked
InvalidCursorError A cursor is malformed, expired, or from another walk
UnprocessableEntityError A parameter failed server-side validation
AuthenticationError The API key is missing or invalid
InsufficientCreditsError The account cannot pay for the request
RateLimitError Too many requests, after retries were exhausted
APITimeoutError The request outlived its timeout, including retries

Client settings

from tapline import SyncTaplineClient

tapline = SyncTaplineClient(
    api_key="your-api-key",
    timeout=120.0,
    max_retries=5,
    default_headers={"X-Trace-Id": "trace-1"},
)

The default timeout is 60 seconds, 10 of them for connecting, because every endpoint proxies YouTube live. Rate limits, 5xx answers, and connection failures are retried twice by default, honoring Retry-After; pass max_retries=0 to disable that.

The client owns a connection pool, so build one and keep it. Close it with tapline.close(), or scope it to a with block as the examples do. To send through your own proxies, transports, or connection limits, pass an http_client — an httpx.Client for the sync client, an httpx.AsyncClient for the async one.

base_url points the client at a proxy or a local server, and can also be set with the TAPLINE_BASE_URL environment variable.

License

MIT. See LICENSE.

Metadata

Release files for tapline 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for tapline 0.2.0
File Size Uploaded Provenance
tapline-0.2.0.tar.gz 200.5 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for tapline 0.2.0
File Interpreter ABI Platform
tapline-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 309.8 kB

Release files / tapline-0.2.0.tar.gz

Download URL tapline-0.2.0.tar.gz
Size 200.5 kB
Tags Source
SHA-256 checksum
How to use checksums
4b36b6a09b485765fee22809a06e1ba5a3740c996706186fad23ad7a0a36dea3
BLAKE2b-256 checksum
How to use checksums
ee01e2dc394351f4d446e4479785159b289df9e8036d673979ea89e3847ac386
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 13, 2026.

Transparency log

Release files / tapline-0.2.0-py3-none-any.whl

Download URL tapline-0.2.0-py3-none-any.whl
Size 109.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
1afb6f07d623a4eb4cb14bc496a195aba04bd61c55c005960a1c55550138d257
BLAKE2b-256 checksum
How to use checksums
608e8dc21ffa18d4947f155389d7c418c913b82cf1cdf26889108ebbc9f10921
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 13, 2026.

Transparency log

Security for tapline 0.2.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for tapline-0.2.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 13, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page