Authentication#

tidalpy speaks Tidal’s OAuth2 device flow (no client secret embedded anywhere in your code) and can also adopt a pasted web token. The saved session lives in one JSON file (or any custom TokenStore).

Checking status#

status = client.auth_status()
# {'logged_in': bool, 'userId': ..., 'countryCode': ...,
#  'clientId': ..., 'tokenType': 'device'|'web',
#  'expired': bool, 'secondsLeft': int|None, 'subscription': {...}}

auth_status() never raises for “not logged in” — it returns logged_in: False. It does try a refresh when the stored token is expired but a refresh token exists.

Subscription / plan#

When logged in, auth_status() carries a subscription view (the raw v1 facts plus two computed tier facts, maxQuality and lossless). For hosts that need to act on the plan, client.subscription() returns the typed Subscription model:

sub = client.subscription()
sub.level                  # "PREMIUM" / "FREE" / ...
sub.max_quality            # Quality.LOSSLESS — the highest deliverable profile
sub.allows_lossless        # bool (reference-client parity)
sub.allows_quality("24/192")  # can this plan deliver Master?

The facts come from the account’s highestSoundQuality ceiling, and the ceiling is finer than the host-facing quality profiles: an HI_RES ceiling delivers LOSSLESS (16/44) but not the HI_RES_LOSSLESS / “Master” (24/192) profile, so allows_quality("24/192") is False for it. A host pre-checks deliverability this way before spending a rate-limited resolve a plan cannot answer. A missing login or a failed lookup degrades to an empty instance (max_quality is None), never an error.

Device flow#

auth = client.start_device_login()
print("1. Open:", auth.verification_url)
print("2. Enter code:", auth.user_code)
client.finish_device_login(auth, timeout=300)  # polls until approved

start_device_login returns a DeviceAuth with the URL the operator must open, the user code, and a poll deadline. finish_device_login polls Tidal (interruptibly — setting the client’s cancellation state stops it) and saves the resulting TokenRecord on success.

Pasted token#

record = client.login("eyJhbGciOi...")

The token is probed against Tidal’s sessions endpoint before being persisted; AuthError on rejection.

Token storage#

  • Default: FileTokenStore at config.token_file — a single JSON document, written atomically.

  • Custom: implement TokenStore (load/save/clear) and pass token_store=... to the client — this is how tidal_transfer keeps sessions in Postgres.

Two rules the library enforces:

  1. A Bearer token is only valid for the OAuth client that issued it — a saved record bound to another client is cleared, not used.

  2. Expiry is tracked (expires_after); refresh happens transparently via the refresh token, and a failed refresh clears the stale session instead of hammering Tidal.

Logout#

client.logout()   # revokes the session remotely, then clears it locally

logout() posts the access token to Tidal’s /v1/logout endpoint (best-effort — a network failure never blocks the logout) and then clears the stored session. It returns whether the remote revocation succeeded, so a host can report “signed out” vs. “signed out locally; the server-side session may linger”. Only the session that was current when the logout started is forgotten: if a new login lands while the revocation is in flight (a host signing a user out on one tab and back in on another), the newer login survives.

login() rejects a purely numeric “token” up front with an actionable message — a numeric paste is almost always a user id, never a Tidal OAuth token, and failing fast saves the probe that would reject it opaquely.