diff --git a/backend/app/auth.py b/backend/app/auth.py
index c654dca..f7f6041 100644
--- a/backend/app/auth.py
+++ b/backend/app/auth.py
@@ -683,7 +683,10 @@ def resolved_user_id(request: Request) -> tuple[int | None, bool]:
"""
default_id = request.session.get("user_id")
wallet = request.session.get("account_ids") or []
- hdr = request.headers.get(ACTIVE_ACCOUNT_HEADER)
+ # Header for normal XHR; ?account= for contexts that can't set headers (WebSocket, and a
+ # plain file download). Both are wallet-gated below, so neither can impersonate an
+ # account that never signed into this browser.
+ hdr = request.headers.get(ACTIVE_ACCOUNT_HEADER) or request.query_params.get("account")
if hdr:
try:
hid = int(hdr)
diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts
index 031be80..5c5d8a1 100644
--- a/frontend/src/lib/api.ts
+++ b/frontend/src/lib/api.ts
@@ -1044,7 +1044,13 @@ export const api = {
req(`/api/downloads/${id}/share`, { method: "POST", body: JSON.stringify({ email }) }),
unshareDownload: (id: number, email: string) =>
req(`/api/downloads/${id}/share/${encodeURIComponent(email)}`, { method: "DELETE" }),
- downloadFileUrl: (id: number): string => `/api/downloads/${id}/file`,
+ // A plain navigation can't send the X-Siftlode-Account header, so pass the active
+ // account via ?account= (same wallet-gated selection the WebSocket uses) — otherwise the
+ // download resolves to the session-default account and 404s for a per-tab account's file.
+ downloadFileUrl: (id: number): string => {
+ const a = getActiveAccount();
+ return a != null ? `/api/downloads/${id}/file?account=${a}` : `/api/downloads/${id}/file`;
+ },
// admin
adminDownloads: (): Promise => req("/api/admin/downloads"),