yt-dlp/yt-dlp · error · ExtractorError

Ticket has expired

Error message

Ticket has expired

What it means

Raised by the ZAN extractor when the getLiveStatus result reports canPlay != True: the viewer's ticket for the stream is no longer playable, i.e. it expired. ZAN issues per-viewer playback tickets; once expired, the API refuses playback even though the stream itself may still be running. Marked expected=True.

Source

Thrown at yt_dlp/extractor/zan.py:183

            self.raise_login_required()

        status = self._download_json(
            f'{self._BASE_URL}/api/live/{video_id}/getLiveStatus', video_id, headers={
                'X-Csrf-Token': csrf_token,
            }, data=urlencode_postdata({
                'pct': pct,
                'token': token,
            }))
        if not traverse_obj(status, ('isSuccess', {bool})):
            raise ExtractorError('Unexpected error')

        result = traverse_obj(status, ('result', {dict}))
        for key, required, error_message in (
            ('isFinished', False, 'This video is no longer available'),
            ('canPlay', True, 'Ticket has expired'),
        ):
            if traverse_obj(result, (key, {bool})) is not required:
                raise ExtractorError(error_message, expected=True)

        is_live = not traverse_obj(result, ('isVod', {bool}))
        release_timestamp = parse_iso8601(self._html_search_meta('open-live-date', webpage))
        srv_time = traverse_obj(status, ('srvTime', {int_or_none}), default=0)

        if is_live and release_timestamp and srv_time < release_timestamp:
            start_time = dt.datetime.fromtimestamp(
                release_timestamp, dt.timezone.utc,
            ).astimezone().strftime('%Y-%m-%d %H:%M:%S %Z')
            self.raise_no_formats(
                f'This livestream is scheduled to start at {start_time}', expected=True)

            return {
                'id': video_id,
                'live_status': 'is_upcoming',
                'release_timestamp': release_timestamp,
            }

View on GitHub (pinned to 81ecd58b13)

Solutions

  1. Re-open the event page in the browser (fresh page load issues a new ticket), re-export cookies, and retry promptly
  2. If the event requires a purchase/login, re-authenticate first so a fresh ticket is granted
  3. Retry the extraction immediately after obtaining the fresh session rather than resuming hours later

Example fix

# before (stale session from an earlier page load)
yt-dlp --cookies old-cookies.txt "https://live.zan.com.br/..."

# after (fresh page visit -> new ticket -> immediate retry)
yt-dlp --cookies-from-browser chrome "https://live.zan.com.br/..."
Defensive patterns

Strategy: retry

Try / catch

from yt_dlp.utils import ExtractorError
try:
    ydl.download([url])
except ExtractorError as e:
    if 'Ticket has expired' in str(e):
        refresh_browser_cookies()  # new page visit issues a fresh ticket
        ydl.download([url])        # retry immediately with the new session
    else:
        raise

Prevention

When it happens

Trigger: Loading the page, waiting (or resuming an interrupted download), then calling the API with a ticket whose validity window elapsed; sharing/duplicating a page session whose ticket was already consumed; the check finds canPlay is not True and raises.

Common situations: Restarting a download much later using stale session cookies; long-running downloads that re-request playback info after a ticket lapse; ticket tied to a single playback session that was already used.

Related errors


AI-assisted analysis of yt-dlp/yt-dlp@81ecd58b13 (2026-08-22). Data as JSON: /api/errors/1a9438bfc55a38a2. Report an issue: GitHub.