{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artifacts.apis.io/layercall-com/json-schema/layercall-com-user-score-schema.json",
  "title": "UserScore",
  "x-generated": "2026-10-31",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/layercall-com-openapi.json#/components/schemas/UserScore",
  "type": "object",
  "required": [
    "risk_score",
    "verdict",
    "components"
  ],
  "properties": {
    "risk_score": {
      "$ref": "#/$defs/RiskScore"
    },
    "verdict": {
      "$ref": "#/$defs/Verdict"
    },
    "components": {
      "type": "object",
      "description": "Each supplied signal, scored independently. Absent keys were not supplied.",
      "properties": {
        "ip": {
          "$ref": "#/$defs/IpScore"
        },
        "email": {
          "$ref": "#/$defs/EmailResult"
        },
        "phone": {
          "$ref": "#/$defs/PhoneResult"
        },
        "domain": {
          "$ref": "#/$defs/DomainResult"
        },
        "device": {
          "$ref": "#/$defs/DeviceScore"
        }
      }
    },
    "request_id": {
      "type": "string"
    },
    "signals_unavailable": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "Signals that could not be measured on this request — e.g. [\"tor\", \"vpn_providers\"]. Absent entirely when every feed answered, so the happy-path shape is unchanged. Present means the score was built on incomplete data: a \"clean\" answer and a \"we could not check\" answer are otherwise identical."
    },
    "summary": {
      "type": "string"
    },
    "components_checked": {
      "type": "array",
      "items": {
        "type": "string"
      }
    },
    "linkage": {
      "type": "object",
      "properties": {
        "device_email_count": {
          "nullable": true
        },
        "email_device_count": {
          "type": "integer"
        },
        "email_ip_count": {
          "type": "integer"
        },
        "subnet_rate_1h": {
          "type": "integer"
        },
        "domain_rate_1h": {
          "type": "integer"
        }
      }
    },
    "actor": {
      "type": "object",
      "properties": {
        "type": {
          "type": "string"
        },
        "proven": {
          "type": "boolean"
        },
        "basis": {
          "type": "string"
        },
        "operator": {
          "nullable": true
        },
        "trigger": {
          "nullable": true
        },
        "detail": {
          "type": "string"
        }
      }
    },
    "top_signals": {
      "type": "array",
      "items": {
        "type": "string"
      }
    },
    "processing_time_sec": {
      "type": "number"
    },
    "billable_lookups": {
      "type": "integer"
    }
  },
  "$defs": {
    "DeviceScore": {
      "type": "object",
      "required": [
        "device_id",
        "risk_score",
        "verdict",
        "bot_probability",
        "signals"
      ],
      "properties": {
        "device_id": {
          "type": "string"
        },
        "risk_score": {
          "$ref": "#/$defs/RiskScore"
        },
        "verdict": {
          "$ref": "#/$defs/Verdict"
        },
        "bot_probability": {
          "type": "integer",
          "minimum": 0,
          "maximum": 100,
          "description": "Reported separately from risk: plenty of automated traffic is legitimate."
        },
        "signals": {
          "type": "object",
          "properties": {
            "is_bot": {
              "type": "boolean",
              "description": "bot_probability >= 60."
            },
            "is_automated": {
              "type": "boolean"
            },
            "is_headless": {
              "type": "boolean"
            },
            "timezone_mismatch": {
              "type": [
                "boolean",
                "null"
              ],
              "description": "Browser timezone country differs from the IP's. Silent (false) when either is genuinely unknown; null when the country could NOT be resolved — a GeoIP outage — so no comparison was made."
            },
            "repeat_device": {
              "type": "boolean",
              "description": "Seen before across the LayerCall network."
            }
          }
        },
        "first_seen": {
          "type": [
            "string",
            "null"
          ],
          "format": "date-time"
        },
        "times_seen": {
          "type": "integer"
        },
        "abuse_reports": {
          "type": "integer"
        },
        "request_id": {
          "type": "string"
        },
        "processing_time_sec": {
          "type": "number"
        },
        "billable_lookups": {
          "type": "integer"
        }
      }
    },
    "DomainResult": {
      "type": "object",
      "required": [
        "domain",
        "risk_score",
        "verdict"
      ],
      "properties": {
        "domain": {
          "type": "string"
        },
        "risk_score": {
          "$ref": "#/$defs/RiskScore"
        },
        "verdict": {
          "$ref": "#/$defs/Verdict"
        },
        "signals": {
          "type": "object",
          "properties": {
            "resolves": {
              "type": "boolean"
            },
            "mx_found": {
              "type": "boolean"
            },
            "has_spf": {
              "type": "boolean"
            },
            "has_dmarc": {
              "type": "boolean"
            },
            "is_disposable": {
              "type": "boolean"
            },
            "is_homograph": {
              "type": "boolean"
            },
            "is_free_provider": {
              "type": "boolean"
            },
            "is_risky_tld": {
              "type": "boolean"
            },
            "newly_registered": {
              "type": [
                "boolean",
                "null"
              ],
              "description": "Registered less than 30 days ago. null means the registration date could not be determined (RDAP unreachable, or the TLD publishes no RDAP) — it does NOT mean the domain is established. Treat null as unknown, not as a negative finding."
            }
          }
        },
        "registration": {
          "type": "object",
          "properties": {
            "created_at": {
              "type": [
                "string",
                "null"
              ],
              "format": "date-time"
            }
          }
        },
        "cached": {
          "type": "boolean"
        },
        "request_id": {
          "type": "string"
        },
        "signals_unavailable": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "Signals that could not be measured on this request — e.g. [\"tor\", \"vpn_providers\"]. Absent entirely when every feed answered, so the happy-path shape is unchanged. Present means the score was built on incomplete data: a \"clean\" answer and a \"we could not check\" answer are otherwise identical."
        },
        "processing_time_sec": {
          "type": "number"
        },
        "first_seen": {
          "nullable": true
        },
        "times_seen": {
          "type": "integer"
        },
        "abuse_reports": {
          "type": "integer"
        },
        "billable_lookups": {
          "type": "integer"
        }
      }
    },
    "EmailResult": {
      "type": "object",
      "required": [
        "email",
        "risk_score",
        "verdict"
      ],
      "properties": {
        "email": {
          "type": "string"
        },
        "normalized_email": {
          "type": "string"
        },
        "risk_score": {
          "$ref": "#/$defs/RiskScore"
        },
        "verdict": {
          "$ref": "#/$defs/Verdict"
        },
        "status": {
          "type": "string",
          "description": "e.g. valid, invalid, do_not_mail, unknown."
        },
        "sub_status": {
          "type": [
            "string",
            "null"
          ],
          "description": "e.g. disposable, role_based, mailbox_not_found."
        },
        "deliverability_score": {
          "type": "integer",
          "minimum": 0,
          "maximum": 100
        },
        "did_you_mean": {
          "type": [
            "string",
            "null"
          ],
          "description": "Suggested correction for a likely typo."
        },
        "signals": {
          "type": "object",
          "properties": {
            "syntax_valid": {
              "type": "boolean"
            },
            "mx_found": {
              "type": "boolean"
            },
            "is_disposable": {
              "type": "boolean"
            },
            "is_homograph": {
              "type": "boolean",
              "description": "Lookalike domain — mixed script or a spelling that folds onto a real ASCII domain."
            },
            "is_role_account": {
              "type": "boolean"
            },
            "is_free_provider": {
              "type": "boolean"
            }
          }
        },
        "cached": {
          "type": "boolean"
        },
        "request_id": {
          "type": "string"
        },
        "signals_unavailable": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "Signals that could not be measured on this request — e.g. [\"tor\", \"vpn_providers\"]. Absent entirely when every feed answered, so the happy-path shape is unchanged. Present means the score was built on incomplete data: a \"clean\" answer and a \"we could not check\" answer are otherwise identical."
        },
        "domain": {
          "type": "string"
        },
        "domain_age_days": {
          "nullable": true
        },
        "mx_provider": {
          "type": "string"
        },
        "mx_records": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "first_seen": {
          "nullable": true
        },
        "times_seen": {
          "type": "integer"
        },
        "abuse_reports": {
          "type": "integer"
        },
        "digital_footprint": {
          "type": "object",
          "properties": {
            "has_gravatar": {
              "type": [
                "boolean",
                "null"
              ],
              "description": "Whether the address has a public Gravatar. null means the check did not run — the request's time budget was exhausted — which is not the same as no avatar."
            },
            "gravatar_profile_url": {
              "nullable": true
            },
            "breach_count": {
              "nullable": true
            },
            "seen_in_breach": {
              "nullable": true
            }
          }
        },
        "processing_time_sec": {
          "type": "number"
        },
        "billable_lookups": {
          "type": "integer"
        }
      }
    },
    "IpScore": {
      "type": "object",
      "required": [
        "ip",
        "risk_score",
        "verdict",
        "signals",
        "geo",
        "cached",
        "request_id"
      ],
      "properties": {
        "ip": {
          "type": "string"
        },
        "risk_score": {
          "$ref": "#/$defs/RiskScore"
        },
        "verdict": {
          "$ref": "#/$defs/Verdict"
        },
        "signals": {
          "type": "object",
          "properties": {
            "is_vpn": {
              "type": "boolean"
            },
            "is_proxy": {
              "type": "boolean"
            },
            "is_datacenter": {
              "type": "boolean"
            },
            "is_tor": {
              "type": "boolean"
            },
            "recent_abuse": {
              "type": "boolean"
            },
            "is_private_or_reserved": {
              "type": "boolean",
              "description": "RFC1918 / loopback / reserved. Cannot be meaningfully scored."
            }
          }
        },
        "geo": {
          "type": "object",
          "properties": {
            "country": {
              "type": [
                "string",
                "null"
              ],
              "description": "ISO 3166-1 alpha-2."
            },
            "city": {
              "type": [
                "string",
                "null"
              ]
            },
            "asn": {
              "type": [
                "string",
                "null"
              ]
            },
            "isp": {
              "type": [
                "string",
                "null"
              ]
            }
          }
        },
        "first_seen": {
          "type": [
            "string",
            "null"
          ],
          "format": "date-time",
          "description": "First time this value was seen across the LayerCall network."
        },
        "times_seen": {
          "type": "integer"
        },
        "abuse_reports": {
          "type": "integer",
          "description": "Confirmed-fraud reports from distinct accounts."
        },
        "cached": {
          "type": "boolean",
          "description": "Served from cache. Cached lookups are never billed."
        },
        "request_id": {
          "type": "string"
        },
        "warning": {
          "type": "string",
          "description": "Present only when the input needs attention."
        },
        "vpn_provider": {
          "type": [
            "string",
            "null"
          ],
          "description": "Name of the VPN operating this exit node (e.g. \"NordVPN\", \"Mullvad\", \"Surfshark\"), taken from the operator's own published server list. null when is_vpn was inferred from an ASN or third-party feed rather than confirmed by the operator — absence is not evidence the IP is not a VPN."
        },
        "signals_unavailable": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "Signals that could not be measured on this request — e.g. [\"tor\", \"vpn_providers\"]. Absent entirely when every feed answered, so the happy-path shape is unchanged. Present means the score was built on incomplete data: a \"clean\" answer and a \"we could not check\" answer are otherwise identical."
        },
        "hijacked_source": {
          "nullable": true
        },
        "processing_time_sec": {
          "type": "number"
        },
        "billable_lookups": {
          "type": "integer"
        }
      }
    },
    "PhoneResult": {
      "type": "object",
      "required": [
        "phone",
        "risk_score",
        "verdict"
      ],
      "properties": {
        "phone": {
          "type": "string"
        },
        "risk_score": {
          "$ref": "#/$defs/RiskScore"
        },
        "verdict": {
          "$ref": "#/$defs/Verdict"
        },
        "parse_status": {
          "type": "string",
          "enum": [
            "ok",
            "country_required",
            "impossible",
            "invalid_pattern"
          ],
          "description": "Distinguishes 'you omitted country' from 'this number is bad'."
        },
        "signals": {
          "type": "object",
          "properties": {
            "syntax_valid": {
              "type": "boolean"
            },
            "is_possible": {
              "type": "boolean"
            },
            "is_voip": {
              "type": [
                "boolean",
                "null"
              ],
              "description": "In a range the national plan reserves for VoIP. null where the plan has no VoIP range (+1 US, India, Germany and others) or the number is only fixed-line-or-mobile: unknown, not no."
            },
            "is_premium_rate": {
              "type": "boolean"
            },
            "is_toll_free": {
              "type": "boolean"
            },
            "assigned_area_code": {
              "type": [
                "boolean",
                "null"
              ],
              "description": "NANP only. null when not applicable."
            }
          }
        },
        "number": {
          "type": "object",
          "properties": {
            "e164": {
              "type": [
                "string",
                "null"
              ]
            },
            "country": {
              "type": [
                "string",
                "null"
              ]
            }
          }
        },
        "cached": {
          "type": "boolean"
        },
        "request_id": {
          "type": "string"
        },
        "processing_time_sec": {
          "type": "number"
        },
        "first_seen": {
          "nullable": true
        },
        "times_seen": {
          "type": "integer"
        },
        "abuse_reports": {
          "type": "integer"
        },
        "billable_lookups": {
          "type": "integer"
        }
      }
    },
    "RiskScore": {
      "type": "integer",
      "minimum": 0,
      "maximum": 100,
      "description": "0 = clean, 100 = certain fraud."
    },
    "Verdict": {
      "type": "string",
      "enum": [
        "allow",
        "review",
        "block"
      ],
      "description": "Recommended action. Thresholds move with `strictness`."
    }
  }
}