Response format
Every check and lookup returns the same typed JSON envelope with verdict, p_scam, reason_codes, classification, entities and signals. Entities carry typed facts and the datasets checked. Signals carry their source class, weight, dataset and coverage. A verdict of unknown means the evidence was not enough or a source failed. Code must never treat unknown as safe. Related domains, assess, watches and events have their own typed objects in the OpenAPI spec.
The envelope
Every check and lookup answers with this object (related domains, assess, watches and events have their own types in the OpenAPI spec). Lookups fill entities with the value you sent and anything it leads to, such as a link’s domain.
| Field | Type | Meaning |
|---|---|---|
schema_version | string | The contract version, 1.8.0 today. |
request_id | string | req_ and a ULID. Quote it in support requests. |
checked_at | timestamp | When the verdict was computed, RFC 3339 in UTC. |
engine | string | The engine version that answered. |
models | object | Only the models actually called for this answer, plus calibration. A lookup calls none. |
input | object | kind (lookup since 1.7.0), channel, language, region. |
verdict | enum | scam, suspicious, safe or unknown. |
p_scam | number 0 to 1 | The probability that the input is part of a scam. |
risk_score | integer 0 to 100 | p_scam times 100, rounded, for simple rules. |
confidence | number 0 to 1 | How settled the decision is, from 0 for unknown to 1. |
classification | null | Filled only by the message and screenshot checks retired in 1.7.0. Kept so a 1.x parser keeps working. |
reason_codes | array | The codes that decided the verdict, strongest first, at most 5. |
entities | array | Everything found and checked, each with its own verdict and facts. |
signals | array | Every signal, attached to an entity. |
degraded | boolean | True when a source or model was skipped or failed. |
degraded_reasons | array | Why, from a fixed list. |
cached | boolean | Served from cache. |
explanation | object or null | Only with explain=true. Plain-language text, not part of the contract. |
Entities
| Field | Meaning |
|---|---|
id | e1, e2 and so on, unique in the response. |
type | url, domain, ip, phone, shortcode, email, wallet, iban, app or payment_handle. |
value, normalized | As sent, and in canonical form: lowercase host, E.164 number. |
role | How it relates to the value you sent: link_target for a link and its domain, redirect_hop for where a short link leads, payee for a wallet, IBAN or payment handle, mentioned for the rest. sender, callback_number, reply_to and hosting stay in the schema from the retired message checks. |
verdict, p_scam, risk_score, reason_codes | The entity’s own judgment. |
facts | Typed facts for its type. Every documented key is always present, null when unknown. |
related | Ids of linked entities: a link and an email address point to their domain. |
datasets | Every dataset checked for this entity, with its geography. in_coverage false means the dataset does not cover it, such as a US-only list for a German number. |
A link is judged together with everything in its related list, so a link to a scam domain is a scam link. In TypeScript, Entity is a union on type, so if (e.type === "domain") narrows e.facts to DomainFacts without a cast. The facts for each type are listed on its page: link, domain, phone, short code, email, IP, wallet, IBAN, app, payment handle.
Signals
| Field | Meaning |
|---|---|
code | A stable reason code. |
entity | The entity id it is about. |
severity | info, low, medium, high or critical. |
weight | Its signed contribution. Positive pushes toward scam, negative reassures. |
source_class | confirmed (a list or report), first_party (our own data), heuristic, model or attacker_content (read from the scammer’s own page or image). |
p | The model’s probability when source_class is model, otherwise null. |
dataset | The dataset the signal came from. |
coverage | That dataset’s geography: WW for worldwide, or a country code such as US or DE. |
evidence | Fixed keys per code, such as the brand a domain imitates. |
How the verdict is decided
The engine weighs every signal, then picks one of four verdicts. p_scam always sits in its verdict’s band:
| Verdict | p_scam | Means |
|---|---|---|
scam | 0.80 to 1 | Clear evidence: a scam-list hit, a brand imitated on an abused domain, a known scam pattern. |
suspicious | 0.50 up to, not including, 0.80 | Real risk that falls short of proof. |
safe | 0 up to, not including, 0.20 | Nothing that should worry the reader. |
unknown | 0.20 up to, not including, 0.50 | Not enough evidence either way, or a source failed (degraded is true). |
signals lists everything found, including signals the decision outweighed: a real 2FA text can carry a medium TEXT_SCAM_WORDING signal and still be safe. reason_codes lists only what decided the verdict. A check that would have been safe but lost a source answers unknown, never safe.
Until the calibrated model ships, models.calibration reads interim-band-v0: p_scam follows the engine’s score inside the verdict’s band and is not yet a measured probability. One part is calibrated already: a domain’s site.p_scam_site, our verdict on a page our worker read, is fitted on labelled hosts and published at GET /v1/calibration/site.
degraded_reasons
source_timeout, source_unavailable, budget_limit, redirect_unresolved, input_truncated, model_unavailable, page_unavailable.
Versioning
The contract was frozen at 1.0.0 and is 1.8.0 today. New fields, new enum values and new reason codes are added in minor versions, so ignore fields you do not know and treat an unknown enum value as unknown. Nothing is renamed or removed within /v1.
Types
Generated from the same models the API is tested against: bewary.d.ts for TypeScript and bewary_models.py for Python (pydantic 2). The OpenAPI spec at /v1/openapi.json carries them too, so code generators type every response.
/** The answer of every lookup and check. */
export interface Envelope {
schema_version: string
request_id: string
/** RFC 3339 UTC. */
checked_at: string
engine: string
models: ModelsUsed
input: InputInfo
verdict: "scam" | "suspicious" | "safe" | "unknown"
/** Probability that the input is part of a scam. */
p_scam: number
risk_score: number
confidence: number
classification: Classification | null
/** The codes that decided the verdict, strongest first, at most 5. */
reason_codes: Array<string>
entities: Array<Entity>
signals: Array<Signal>
degraded: boolean
degraded_reasons: Array<"source_timeout" | "source_unavailable" | "budget_limit" | "redirect_unresolved" | "input_truncated" | "model_unavailable" | "page_unavailable">
cached: boolean
explanation: Explanation | null
}
export interface Signal {
/** A stable reason code, https://bewary.app/docs/reason-codes */
code: string
/** The entity id, or "message". */
entity: string
severity: "info" | "low" | "medium" | "high" | "critical"
/** Signed contribution: positive toward scam, negative reassuring. */
weight: number
source_class: "confirmed" | "first_party" | "heuristic" | "model" | "attacker_content"
/** Model probability when source_class is model. */
p: number | null
/** The dataset the signal came from, https://bewary.app/data */
dataset: "registration" | "dns" | "certificates" | "web_archive" | "threat_feeds" | "threat_aggregates" | "brand_registry" | "web_pages" | "ip_network" | "phone_numbering" | "us_numbering" | "phone_complaints" | "honeypot_lines" | "phone_reports_de" | "shortcodes" | "email_providers" | "breaches" | "wallet_reports" | "sanctions" | "community_reports" | "campaigns" | "text_model" | "platform_pages" | "iban_registry" | "bank_codes" | "app_stores" | "short_links" | "payment_profiles"
/** That dataset's geography: WW (worldwide) or a country code (US, DE). */
coverage: string
evidence: Record<string, unknown>
} Updated · This page as Markdown · Changelog