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.
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
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 unknownimport 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.
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.
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 rWhen 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.
Updated · This page as Markdown · Changelog