Every request to /api/v1/* requires an API key created from the Customer Portal, sent as a bearer token. A key only has access to the scopes it was created with, and those scopes are themselves capped by your plan (see Scopes Reference).
curl http://localhost/api/v1/ip/check?ip=1.2.3.4 \
-H "Authorization: Bearer wsk_xxx_xxx"
A request with a missing/invalid key returns 401; a valid key missing the required scope returns 403 insufficient_scope; exceeding your plan's rate limit or request quota returns 429.
Requires scope ip.check. Returns geolocation, ASN, VPN/proxy/TOR/hosting flags, threat-list status, and a 0-100 fraud score with reasons.
Params: ip (optional — defaults to the caller's own IP)
{
"data": {
"ip": "1.2.3.4",
"country": "US", "region": null, "city": null,
"asn": null, "isp": null, "organization": null,
"is_vpn": false, "is_proxy": false, "is_tor": false,
"is_hosting": false, "is_mobile": false, "is_private": false,
"is_whitelisted": false, "is_blacklisted": false,
"fraud_score": 5, "risk_score": 5, "confidence": 40,
"reasons": ["No risk signals detected"]
}
}
Requires scope asn.check. Looks up ASN ownership and whether it's a hosting/datacenter provider.
Params: asn (e.g. 15169 or AS15169) or ip — one is required.
{
"data": {
"asn": 15169, "name": "GOOGLE", "country": "US",
"registry": "arin", "allocated": "2000-03-30",
"prefix": "8.8.8.0/24", "is_hosting_provider": true
}
}
Requires scope email.check. Checks disposable-domain status, MX records, and role-account (admin@, support@...) patterns.
Params: email (required)
{
"data": {
"email": "[email protected]", "domain": "example.com",
"is_valid_syntax": true, "is_disposable": false,
"has_mx": true, "mx_hosts": ["mx1.example.com"],
"is_role_account": false, "has_gravatar": null,
"domain_registration": null,
"fraud_score": 0, "risk_score": 0,
"reasons": ["No risk signals detected"]
}
}
Requires scope phone.check. Validates a phone number and flags VOIP/disposable lines.
Params: phone (required), country (optional ISO 3166-1 alpha-2 region hint for numbers given without a country code)
{
"data": {
"valid": true, "e164": "+12025551234", "is_voip": false,
"phone": "2025551234", "is_disposable": false,
"fraud_score": 0, "risk_score": 0,
"reasons": ["No risk signals detected"]
}
}
Requires scope domain.check. Checks DNS resolvability and blacklist status; registration (WHOIS/RDAP) and SSL certificate detail are included only if enabled in Admin Settings for this module (off by default).
Params: domain (required)
{
"data": {
"domain": "example.com", "has_dns": true,
"is_blacklisted": false, "registration": null, "ssl_certificate": null,
"fraud_score": 0, "risk_score": 0,
"reasons": ["No risk signals detected"]
}
}
Requires scope url.check. Checks a full URL and its host against URL/domain blacklists (phishing/malware feeds) and DNS resolvability.
Params: url (required, must be http/https)
{
"data": {
"url": "https://example.com/path", "host": "example.com",
"is_valid": true, "has_dns": true,
"is_url_blacklisted": false, "is_domain_blacklisted": false,
"is_phishing": false,
"fraud_score": 0, "risk_score": 0,
"reasons": ["No risk signals detected"]
}
}
Requires scope dns.check. Checks an IPv4 address against configured DNSBL zones (Spamhaus and others).
Params: ip (required, IPv4 only)
{
"data": {
"ip": "1.2.3.4", "is_listed": false, "listed_on": [],
"fraud_score": 0, "risk_score": 0,
"reasons": ["No risk signals detected"]
}
}
Both require scope bot.check. bot/check classifies a User-Agent string (automation tool, HTTP library, or a claimed search-engine crawler) and, given an ip, verifies a claimed crawler's identity by reverse-DNS. crawler/verify does the reverse-DNS identity check directly against an IP.
bot/check params: user_agent (required), ip (optional, enables crawler verification). crawler/verify params: ip (required)
// GET /api/v1/bot/check
{
"data": {
"user_agent": "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)",
"ip": "66.249.66.1", "is_bot": true,
"category": "verified_crawler", "matched_name": "Googlebot",
"is_verified_crawler": true, "is_spoofed_crawler": false,
"crawler_verification": {"ptr": "crawl-66-249-66-1.googlebot.com", "name_from_dns": "Googlebot"},
"fraud_score": 0, "risk_score": 0,
"reasons": ["No bot signals detected"]
}
}
Requires scope device.check. Hashes browser fingerprint signals collected client-side (via the WSDeviceFingerprint JS helper) into a stable device ID, tracks repeat sightings, and flags headless/automation browsers.
JSON body: any of canvas, webgl_vendor, webgl_renderer, fonts, audio, screen, timezone, language, languages, hardware_concurrency, device_memory, platform, webdriver, plugins_count, touch_support, user_agent
{
"data": {
"device_hash": "a1b2c3...", "is_new_device": false, "times_seen": 4,
"first_seen": "2026-06-01 10:00:00", "is_headless_suspected": false,
"fraud_score": 0, "risk_score": 0,
"reasons": ["No headless/automation signals detected"]
}
}
Requires scope risk.check. Records an action against an entity (IP, email, phone, device, account, cookie, or a custom identifier) and evaluates velocity (rate of repeated actions), account age, and — for entity_type=cookie — cross-session cookie consistency.
Params: entity_type (ip/email/phone/device/account/cookie/custom, required), entity_value (required), action (required, e.g. login/signup/checkout), account_created_at (optional), device_hash (optional, for cookie checks)
{
"data": {
"entity_type": "email", "entity_value": "[email protected]", "action": "login",
"counts": {"60s": 1, "300s": 1, "3600s": 1, "86400s": 1},
"velocity_exceeded": false, "exceeded_window": null,
"account_age_seconds": null, "is_new_account": false,
"cookie_consistency": null,
"fraud_score": 0, "risk_score": 0,
"reasons": ["No risk signals detected"]
}
}
POST /api/v1/interaction/check (scope interaction.check) scores a single form/page's mouse-and-keystroke timing summary — produced client-side by WSInteractionTelemetry.summarize() — for signs of scripted/automated submission.
// POST /api/v1/interaction/check — body: time_to_first_interaction_ms, time_to_submit_ms,
// mouse_move_count, mouse_move_interval_stdev_ms, keystroke_count, keystroke_dwell_avg_ms, ...
{
"data": {
"is_suspected_automated": false,
"fraud_score": 0, "risk_score": 0,
"reasons": ["No automation timing signals detected"]
}
}
POST /api/v1/interaction/session-beacon and GET /api/v1/interaction/session/{sessionId} (both scope interaction.session) accumulate and then score an entire session's mouse/keyboard/focus timeline, for longer-running risk decisions than a single form submit.
GET /api/v1/lists/check (scope lists.read) checks whether a value is on your blacklist/whitelist. POST /api/v1/lists (scope lists.write) adds one.
check params: entity_type (default ip), value. store params: list_type (blacklist/whitelist), entity_type, value, reason (optional)
// GET /api/v1/lists/check?entity_type=ip&value=1.2.3.4
{"data": {"entity_type": "ip", "value": "1.2.3.4", "is_blacklisted": false, "is_whitelisted": false}}
POST /api/v1/reports (scope report.submit) files an abuse report against an entity. GET /api/v1/reports/{id} (scope report.fetch) retrieves one back.
submit params: entity_type (default ip), value, category (default fraud), notes (optional)
// POST /api/v1/reports
{"data": {"id": 42, "status": "pending"}}
Requires scope screen.check. One consolidated call that screens an entire order/transaction — IP, billing/shipping address, email, phone, username, card BIN, and device — reusing the same checks as this API's other endpoints, and returns a single shield_score/shield_status decision plus any matched Workflow Automation rules.
Params (all optional except ip, which defaults to the caller's own IP if omitted): ip, user_order_id, amount, currency, email, phone, username, device_hash, bin_no, bill_country, bill_state, bill_city, bill_zip_code, ship_country, ship_state, ship_city, ship_zip_code, ship_addr. Any field left out simply comes back null in its section rather than a guessed value — pass more params to get more real signal.
bin_no is looked up in a local BIN database first; on a miss it falls back to a live lookup against binlist.net and caches the result locally, so repeat BINs are served instantly and offline afterward.
amount/currency (currency as a 3-letter code, e.g. USD) feed a daily per-email sales-amount threshold check — a submitted card BIN feeds a daily per-BIN attempt-count threshold check. Both are available as real Workflow Automation conditions (daily_amount_exceeded, card_attempts_exceeded, card_submitted) and both thresholds are admin-configurable. Amounts are never converted between currencies — the daily total only ever accumulates within one currency at a time.
curl -X POST http://localhost/api/v1/screen \
-H "Authorization: Bearer wsk_xxx_xxx" \
-d "ip=103.148.154.174&amount=10.00¤cy=USD&email=user@example.com&bin_no=411111"
{
"data": {
"ip_geolocation": {
"ip": "103.148.154.174", "continent": null, "country_code": null, "country_name": null,
"region": null, "city": null, "latitude": null, "longitude": null,
"zip_code": null, "timezone": null, "isp_name": null, "domain": null,
"netspeed": null, "mobile_mnc": null, "mobile_mcc": null, "mobile_brand": null, "elevation": null,
"usage_type": [], "is_proxy": false, "proxy_type": null, "is_in_blacklist": false,
"fraud_score": 0, "risk_reasons": ["No risk signals detected"]
},
"billing_address": {"ip_distance_in_km": null, "ip_distance_in_mile": null, "is_ip_country_match": null},
"shipping_address": {
"is_address_ship_forward": null, "is_bill_country_match": null, "is_bill_state_match": null,
"is_bill_city_match": null, "is_bill_postcode_match": null,
"is_export_controlled_country": null, "is_in_blacklist": false
},
"email_address": {"is_free": false, "is_disposable": false, "is_domain_exist": true, "is_new_domain_name": null, "is_in_blacklist": false},
"phone_number": {"is_disposable": null, "is_in_blacklist": null},
"username": {"is_high_risk": null, "is_in_blacklist": null},
"credit_card": {
"card_brand": "Visa", "card_type": "Debit", "card_subtype": "Classic",
"card_issuing_bank": null, "card_issuing_country": null,
"is_prepaid": false, "is_bin_exist": true, "is_bin_country_match": null, "is_in_blacklist": false
},
"device": {"is_malware_exploit": null, "is_in_blacklist": null},
"smart_detection": {
"ip_location_anomaly": null, "ship_location_anomaly": null,
"ip_anomaly_speed_kmh": null, "ip_anomaly_distance_km": null
},
"user_order_id": "", "shield_id": "20260805-36B0F1",
"shield_score": 0, "shield_status": "APPROVE",
"shield_reasons": ["No risk signals detected"], "shield_rules": [],
"api_version": "1.0.0",
"requests_remaining_today": 200, "requests_remaining_this_month": 1000
}
}
Several fields are honestly null rather than estimated, because this app has no real data source for them: zip_code/netspeed/mobile_mnc/mobile_mcc/mobile_brand/elevation (not in the free GeoLite2 database), ip_distance_in_km/ip_distance_in_mile (no geocoding capability), is_address_ship_forward (no freight-forwarder database), and device.is_malware_exploit (this app detects headless/automation browsers, a different signal). shield_status is APPROVE/REVIEW/DECLINE based on shield_score against your Api module settings' approve/review thresholds. shield_rules lists the names of any active Workflow Automation rules matched by this screening.
smart_detection compares this order against the same email's most recent prior order (default lookback: 24 hours — Api.geo_anomaly_window_hours). ip_location_anomaly ("impossible travel") fires when the implied speed between the two orders' IP locations exceeds a plausible threshold (default 1000 km/h — Api.geo_anomaly_max_speed_kmh); movement under 50km never fires regardless of speed. ship_location_anomaly fires when the shipping country changed between the two orders. Both are honestly null, not false, when no prior order for that email exists within the window — there's nothing to compare against yet.
Requires scope screen.check. Replays the exact response from a previous POST /api/v1/screen call by its shield_id, without recomputing anything — useful for re-displaying an order's decision later without a fresh check. Scoped to your own account: a shield_id from another account's key returns 404. By default this call does not count toward your daily/monthly request quota (the per-minute rate limit still applies) — an admin can change this per endpoint under Admin > API Endpoints.
curl http://localhost/api/v1/screen/20260805-36B0F1 \
-H "Authorization: Bearer wsk_xxx_xxx"
Requires scope screen.feedback. Reports the real outcome of an order back to WHA Shield after the fact — a screening decision doesn't have to be a one-shot guess. Scoped to your own account, same as the lookup above: a shield_id from another account's key returns 404.
Params: action (required) — one of mark_fraud, mark_legitimate, block_ip — and reason (optional, free text).
block_ip adds the screened IP to your blacklist — the same list POST /api/v1/lists and every future screen/ip/check call against it consult.mark_fraud / mark_legitimate records the outcome (confirmed_fraud / false_positive) against a Case Management case for this IP — real ground-truth data the trainable risk scorer learns from over time, not just a label.curl -X POST http://localhost/api/v1/screen/20260805-36B0F1/feedback \
-H "Authorization: Bearer wsk_xxx_xxx" \
-d "action=mark_fraud&reason=Chargeback confirmed by processor"
{
"data": {
"action": "mark_fraud", "ip": "103.148.154.174",
"case_id": 7, "outcome": "confirmed_fraud"
}
}
mark_fraud/mark_legitimate return 503 feedback_unavailable if the Case Management module isn't installed/migrated on this instance — block_ip has no such dependency and always works.
Requires scope account. Returns the calling user and the calling API key's own name, scopes, and rate limit — useful for a client to self-check what it's allowed to do.
{
"data": {
"user": {"id": 12, "name": "Jane Doe", "email": "[email protected]"},
"key": {"name": "Production", "scopes": ["ip.check", "email.check"], "rate_limit_per_minute": 60}
}
}
Every response carries X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers for the per-minute limit. Your plan also sets a monthly and daily request quota; exceeding either returns 429 quota_exceeded with a message naming which one. All three limits (rate/day/month) are set per plan under Billing — see Pricing for the current plan limits, or GET /api/v1/account for your key's own rate limit.
Assign only the scopes a key needs when creating it in the Customer Portal. A plan may also restrict which scopes can be assigned at all — a key creation request for a scope your plan doesn't include is dropped, and the portal's key-creation form greys those out.
| Scope | Endpoint(s) |
|---|---|
ip.check | GET /ip/check |
asn.check | GET /asn/check |
email.check | GET /email/check |
phone.check | GET /phone/check |
domain.check | GET /domain/check |
url.check | GET /url/check |
dns.check | GET /dnsbl/check |
bot.check | GET /bot/check, GET /crawler/verify |
device.check | POST /device/check |
risk.check | POST /behavior/check |
interaction.check | POST /interaction/check |
interaction.session | POST /interaction/session-beacon, GET /interaction/session/{id} |
lists.read | GET /lists/check |
lists.write | POST /lists |
report.submit | POST /reports |
report.fetch | GET /reports/{id} |
screen.check | POST /screen, GET /screen/{shieldId} |
screen.feedback | POST /screen/{shieldId}/feedback |
account | GET /account |