# Act on a verdict

Every Bewary answer carries one of four verdicts. Block or warn on scam, warn on suspicious, let safe through, and never treat unknown as safe: unknown means the evidence was not enough or a source failed. The code below does that for an inbox, a sign-up form and a link preview, with the retries and timeouts that match each endpoint.

Updated 2026-10-30.

## What each verdict means and what to do

| Verdict | Inbox or chat | Sign-up form | Link preview | AI agent |
|---|---|---|---|---|
| `scam` | Warn loudly, hide the link | Reject | No preview, mark the link dangerous | Stop, tell the user why |
| `suspicious` | Warn and show why | Reject, or ask for another address | Show the bare link, no preview card | Ask the user first |
| `safe` | No warning | Accept | Full preview | Continue |
| `unknown` | Say it could not be verified | Accept (a Gmail address answers unknown) | Show the bare link, no preview card | Ask the user first |

Never treat `unknown` as safe. A check that would have been safe but lost a source answers `unknown`, never `safe`. Verdicts can be wrong, so when one blocks a person, such as a rejected sign-up, show them a way to reach you. Build a preview card (fetch the page from your server) only for a `safe` link: fetching a scam page can itself trigger it.

## One function for all four

```python
def decide(result, context="inbox"):
    """Map a Bewary answer to an action. context: inbox, signup or link."""
    verdict = result["verdict"]
    if context == "signup":                # a throwaway address answers suspicious
        return "reject" if verdict in ("scam", "suspicious") else "accept"
    if verdict == "scam":
        return "block"
    if verdict == "safe":
        return "allow"
    return "warn"            # suspicious or unknown
```

```typescript
import type { Envelope } from "./bewary"

export function decide(r: Envelope, context: "inbox" | "signup" | "link" = "inbox") {
  if (context === "signup") return r.verdict === "scam" || r.verdict === "suspicious" ? "reject" : "accept"
  if (r.verdict === "scam") return "block"
  if (r.verdict === "safe") return "allow"
  return "warn"              // suspicious or unknown
}
```

## Show the user why

`reason_codes` lists why, strongest first. For a lookalike link or domain, the domain entity's `facts.lookalike.brand_id` is the brand it imitates. Turn the first code into a sentence your users understand, or name the brand: "This link imitates USPS". Every code is listed on the [reason codes page](/docs/reason-codes).

## Finer control

`p_scam` (0 to 1) and `risk_score` (0 to 100) let you set your own line, such as warning from 0.5. Until the calibrated model ships, `models.calibration` reads `interim-band-v0` and the number follows the verdict's band, so prefer the verdict for now.

## Timeouts, retries and failures

| Call | Measured | Timeout to set |
|---|---|---|
| Lookups | median 113 to 477 ms | 10 seconds |
| Deep link or domain lookup | Longer: it reads the page | 60 seconds |
| Batch of 1,000 | depends on the mix | 120 seconds |

Retry `429` and `503` after the `Retry-After` header, and a `500` once. Do not retry `400`, `401` or `402`: the request or the key has to change. A `401` means the key is missing or wrong: every call needs one.

```python
import time, requests

def bewary(method, path, timeout=10, max_wait=5, **kwargs):
    """One API call with the retries the API supports. max_wait is the longest Retry-After
    worth waiting for: keep it short in a request a person is waiting on."""
    r = None
    for attempt in range(3):
        try:
            r = requests.request(method, "https://api.bewary.app" + path, timeout=timeout, **kwargs)
        except (requests.Timeout, requests.ConnectionError):
            if attempt == 2:
                raise
            continue
        retry_after = r.headers.get("Retry-After", "")
        wait = int(retry_after) if retry_after.isdigit() else 2
        if r.status_code in (429, 503) and attempt < 2 and wait <= max_wait:
            time.sleep(wait)               # a daily-limit 429 waits hours: it is returned instead
            continue
        if r.status_code >= 500 and r.status_code != 503 and attempt == 0:
            continue                       # our failure: retry once
        return r
    return r
```

When the API cannot answer, decide on purpose: an inbox should show "not checked" rather than "safe", and a sign-up form should accept rather than block real customers during our outage.

## Caching

Lookups reuse recent source data, such as a domain's registration date, for up to a week. Store verdicts on your side too: re-checking the same link on every page view costs credits and adds nothing.
