usestrix/strix · warning · RelayError
work_email_required
work_email_required
Error message
work_email_required
What it means
RelayError('work_email_required') raised by otp_start() when POST /api/oss/otp/start returns 400 with body error='work_email_required'. The relay deliberately rejects free/personal email domains (gmail.com, outlook.com, etc.) — it wants a work address — and this code distinguishes that policy rejection from a malformed address.
Source
Thrown at strix/interface/viewer/auth.py:176
try:
data = json.loads(raw or b"{}")
except json.JSONDecodeError:
return {}
return data if isinstance(data, dict) else {}
def otp_start(email: str) -> None:
"""Ask the relay to email a verification code. Raises RelayError on failure."""
status, data = _post_json("/api/oss/otp/start", {"email": email}, timeout=_OTP_TIMEOUT)
if status == 200:
return
if status == 429:
raise RelayError("rate_limited")
if status == 400:
# The relay uses 400 both for a malformed address and, separately, to
# reject a free/personal email domain (it wants a work email).
if data.get("error") == "work_email_required":
raise RelayError("work_email_required")
raise RelayError("invalid_email")
raise RelayError("unavailable")
def otp_verify(email: str, code: str) -> dict[str, Any]:
"""Verify a code. Returns ``{token, email, expires_at}`` or raises RelayError."""
status, data = _post_json(
"/api/oss/otp/verify",
{"email": email, "code": code},
timeout=_OTP_TIMEOUT,
)
if status == 200 and isinstance(data.get("token"), str):
# A token with no usable expiry cannot unlock history locally (the gate
# fails closed), so treat such a response as a failed verification rather
# than reporting success and then leaving the user stuck unverified.
if parse_expiry(data.get("expires_at")) is None:
raise RelayError("unavailable")
return dataView on GitHub (pinned to 8551339130)
Solutions
- Use a corporate/work domain email address for verification
- If you only have a personal address, use the viewer without verification — core local viewing works; only relay-backed extras (history unlock, report delivery) need it
Defensive patterns
Strategy: validation
Validate before calling
FREE_DOMAINS = {"gmail.com", "outlook.com", "hotmail.com", "yahoo.com", "icloud.com"}
def is_work_email(email: str) -> bool:
domain = email.rsplit("@", 1)[-1].lower()
return "@" in email and domain not in FREE_DOMAINS Try / catch
try:
otp_start(email)
except RelayError as e:
if e.code == "work_email_required":
show("Use a work email address; personal domains are not accepted.")
else:
raise Prevention
- Collect a corporate-domain email up front in verification UIs
- Explain the work-email requirement in the form's help text
- Fall back to unverified local usage if no work email exists
When it happens
Trigger: Calling otp_start('user@gmail.com') or any address on a consumer/freemail domain. The relay returns 400 with {"error": "work_email_required"} and the client maps it to this specific RelayError code.
Common situations: Individual researchers or hobbyists who only have personal email accounts trying to unlock viewer history/report features; company emails that route through a gmail frontend (domains verified as freemail by the relay's domain list).
Related errors
AI-assisted analysis of usestrix/strix@8551339130 (2026-08-15).
Data as JSON: /api/errors/689400da3ad5fab3.
Report an issue: GitHub.