Ask ChatGPT Ask Claude

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.

FieldTypeMeaning
schema_versionstringThe contract version, 1.8.0 today.
request_idstringreq_ and a ULID. Quote it in support requests.
checked_attimestampWhen the verdict was computed, RFC 3339 in UTC.
enginestringThe engine version that answered.
modelsobjectOnly the models actually called for this answer, plus calibration. A lookup calls none.
inputobjectkind (lookup since 1.7.0), channel, language, region.
verdictenumscam, suspicious, safe or unknown.
p_scamnumber 0 to 1The probability that the input is part of a scam.
risk_scoreinteger 0 to 100p_scam times 100, rounded, for simple rules.
confidencenumber 0 to 1How settled the decision is, from 0 for unknown to 1.
classificationnullFilled only by the message and screenshot checks retired in 1.7.0. Kept so a 1.x parser keeps working.
reason_codesarrayThe codes that decided the verdict, strongest first, at most 5.
entitiesarrayEverything found and checked, each with its own verdict and facts.
signalsarrayEvery signal, attached to an entity.
degradedbooleanTrue when a source or model was skipped or failed.
degraded_reasonsarrayWhy, from a fixed list.
cachedbooleanServed from cache.
explanationobject or nullOnly with explain=true. Plain-language text, not part of the contract.

Entities

FieldMeaning
ide1, e2 and so on, unique in the response.
typeurl, domain, ip, phone, shortcode, email, wallet, iban, app or payment_handle.
value, normalizedAs sent, and in canonical form: lowercase host, E.164 number.
roleHow 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_codesThe entity’s own judgment.
factsTyped facts for its type. Every documented key is always present, null when unknown.
relatedIds of linked entities: a link and an email address point to their domain.
datasetsEvery 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

FieldMeaning
codeA stable reason code.
entityThe entity id it is about.
severityinfo, low, medium, high or critical.
weightIts signed contribution. Positive pushes toward scam, negative reassures.
source_classconfirmed (a list or report), first_party (our own data), heuristic, model or attacker_content (read from the scammer’s own page or image).
pThe model’s probability when source_class is model, otherwise null.
datasetThe dataset the signal came from.
coverageThat dataset’s geography: WW for worldwide, or a country code such as US or DE.
evidenceFixed 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:

Verdictp_scamMeans
scam0.80 to 1Clear evidence: a scam-list hit, a brand imitated on an abused domain, a known scam pattern.
suspicious0.50 up to, not including, 0.80Real risk that falls short of proof.
safe0 up to, not including, 0.20Nothing that should worry the reader.
unknown0.20 up to, not including, 0.50Not 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.

bewary.d.ts, core interfaces
/** 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