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 data

View on GitHub (pinned to 8551339130)

Solutions

  1. Use a corporate/work domain email address for verification
  2. 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

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.