Skip to main content

Turn Fraud Signals into Helpful Guidance

When you protect a signup or a form with the Fraud Protection API, the response tells you far more than "risky" or "safe". It tells you why. A submission can score high because someone is attacking you, or because a real customer typed gnail.com instead of gmail.com, forgot a character in their address, or happens to be on a corporate VPN.

If you block every high and highest score, you stop a lot of fraud. You also turn away legitimate people who made an honest mistake, and they usually leave without telling you why. This guide shows a better pattern: use the individual signals in the response to guide the user to fix the fixable problems, and reserve hard blocking for the cases that are genuinely unrecoverable.

The result is a signup flow that is both safer and more forgiving. Opportify scores each submission across its signals; you stay in control of what happens next.

Who this guide is for
  • Product and UX readers get a clear picture of what each situation looks like to the end user and what message to show.
  • Engineers get the exact response attributes to read, the values to check, and copy-paste-ready decision logic that works in any language.

You can hand this whole page to an AI coding assistant and it has everything it needs to implement the flow correctly.


The core idea: guide, don't just block

Most teams start with a single rule: "if the score is high, reject the submission." That rule throws away the most valuable part of the response.

The Fraud Protection API returns a top-level score and level, and it also returns a detailed breakdown under sources. Each source (email, IP, content, and more) carries specific, human-understandable signals. Instead of reacting only to the final number, read the signals and sort every submission into one of three buckets:

OutcomeWhat it meansWhat the user experiences
AllowNo blocking signal, acceptable riskThe submission goes through normally
GuideThe problem is something the user can fixA clear, friendly message on the specific field, and a chance to correct it
Block or step upUnrecoverable or clearly abusiveA firm message, or a challenge such as extra verification, per your policy

The "Guide" bucket is where you recover legitimate users. A mistyped email, a non-existent mailbox, or a disposable address are all things a real person can correct in seconds if you tell them clearly what to do.

You decide the policy

Opportify surfaces the signals and the risk score. The thresholds, the wording, and the final decision are yours. Everything in this guide is a recommended default you can tune to your own risk appetite.


What you send

At minimum, send the user's email and IP address. Passing both unlocks email deliverability signals, IP connection signals, and geographic cross-referencing in a single call.

Send the request from your backend, never the browser, so your API key stays private.

POST https://api.opportify.ai/intel/v1/fraud/analyze
Host: api.opportify.ai
Content-Type: application/json
x-opportify-token: YOUR_API_KEY

{
"email": "jhon.doe@gnail.com",
"userIp": "203.0.113.42",
"submissionType": "registration"
}
  • email and userIp are the two fields this guide relies on. At least one of them is required.
  • submissionType is optional but recommended. It labels the action ("registration", "signup", "checkout", and so on) so you can filter and report on it later.
  • You can add name, phone, address, and message fields for deeper analysis. They are optional and do not change the field-guidance logic below.

For the full request and response schema, see the Analyze endpoint reference.


What you get back

The response has a top-level summary and a per-source breakdown. The snippet below shows every top-level key the response contains, with only the attributes this guide uses expanded. The ... markers indicate that each source returns many more fields (full domain enrichment, WHOIS, geo, DNS, and so on). See the full sample response at the end of this section for a complete payload.

{
"score": 720,
"level": "high",
"factors": ["email:disposable-domain", "email:instant-bounce", "email:no-mx-or-invalid"],
"sources": {
"email": {
"emailAddress": "jane.doe@example.com",
"emailType": "disposable",
"isDeliverable": "no",
"isFormatValid": true,
"emailCorrection": "",
"addressSignals": { "isRoleAddress": false, "isNoReply": false, "...": "more fields" },
"riskReport": { "score": 720, "level": "high", "baseAnalysis": ["disposable-domain"] },
"...": "emailProvider, emailDNS, domain, and more"
},
"ip": {
"ipAddress": "203.0.113.42",
"connectionType": "cloud-provider",
"blocklisted": { "isBlockListed": false, "activeReports": 0 },
"riskReport": { "score": 318, "level": "low" },
"...": "geo, whois, trustedProvider, and more"
},
"content": "... (or null)",
"session": "... (or null)",
"velocity": "... (or null)",
"geo": "... (or null)"
}
}

Two things to know before you write any logic:

  1. sources.email is present only when you send an email. sources.ip is present only when you send a userIp. Always check that a source exists before reading its fields.
  2. isDeliverable is a string, not a boolean. Its value is "yes", "no", or "unknown". Compare it as text.

The score runs from 200 (lowest risk) to 1000 (highest risk), and level is the matching bucket:

levelscore range
lowest200 to 300
low301 to 400
medium401 to 600
high601 to 800
highest801 to 1000
Full sample response (email + IP)

This is a complete payload returned when both email and userIp are supplied. Your integration only needs the handful of attributes covered in this guide, but this shows everything that is available.

{
"score": 720,
"level": "high",
"factors": ["email:disposable-domain", "email:instant-bounce", "email:no-mx-or-invalid"],
"sources": {
"email": {
"emailAddress": "jane.doe@example.com",
"emailProvider": "Helicopter-mail",
"emailType": "disposable",
"isDeliverable": "no",
"isCatchAll": false,
"isMailboxFull": false,
"isReachable": false,
"isFormatValid": true,
"emailCorrection": "",
"addressSignals": {
"tagDetected": false,
"tagValue": "",
"normalizedAddress": "jane.doe@example.com",
"isRoleAddress": false,
"roleType": "",
"isNoReply": false,
"noReplyPattern": ""
},
"emailDNS": {
"mx": ["0 "],
"spfValid": true,
"dkimConfigured": true,
"dmarcValid": true,
"mxRelay": false,
"mxRelayCategory": ""
},
"riskReport": {
"score": 720,
"level": "high",
"baseAnalysis": ["disposable-domain", "instant-bounce", "no-mx-or-invalid"]
},
"domain": {
"name": "example.com",
"enrichmentAvailable": true,
"creationDate": "1995-08-14T04:00:00.000Z",
"expirationDate": "2027-08-13T04:00:00.000Z",
"updatedDate": "2026-08-14T08:01:43.000Z",
"ageYears": 31,
"registrar": "RESERVED-Internet Assigned Numbers Authority",
"isBlockListed": false,
"mtaStsStatus": "absent",
"bimiStatus": "absent",
"hasVMC": false,
"aRecordValid": true,
"aRecordReverseHost": "a23-220-75-245.deploy.static.akamaitechnologies.com",
"sslValid": true
}
},
"ip": {
"ipAddress": "8.8.8.8",
"ipAddressNumber": 134744072,
"ipType": "IPv4",
"ipCidr": "8.8.8.0/24",
"connectionType": "cloud-provider",
"hostReverse": "dns.google",
"geo": {
"continent": "NA",
"countryCode": "US",
"countryName": "United States",
"city": "Mountain View",
"latitude": 37.38605,
"longitude": -122.08385,
"timezone": "America/Los_Angeles"
},
"whois": {
"rir": "ARIN",
"asn": { "asnId": "AS15169" },
"organization": { "orgId": "GOGL", "orgName": "GOOGLE LLC", "country": "US" }
},
"trustedProvider": { "isKnownProvider": false },
"blocklisted": { "isBlockListed": false, "sources": 0, "activeReports": 0 },
"riskReport": { "score": 318, "level": "low" }
},
"content": {
"riskReport": { "score": 200, "level": "lowest", "baseAnalysis": [] }
},
"session": null,
"velocity": {
"emailSubmissions": { "last60s": 2, "last300s": 2, "last3600s": 2, "last86400s": 2 },
"ipSubmissions": { "last60s": 1, "last300s": 1, "last3600s": 1, "last86400s": 1 },
"phoneSubmissions": null,
"anomaly": false,
"riskReport": { "score": 200, "level": "lowest", "baseAnalysis": [] }
},
"geo": null
}
}

The signal-to-message map

This is the heart of the guide. Each row is one situation you can detect, the exact attributes that describe it, and a suggested message to show the user. The Field column tells you which input to highlight, and the Suggested action column tells you whether to guide the user or hold the submission.

Email situations

Email guidance is about data quality, not just risk. Show these messages whenever the matching signal is present, regardless of the overall score. A mistyped or undeliverable address is worth correcting even on an otherwise low-risk submission, because it protects your deliverability and your data.

SituationHow to detect it (exact attributes)Field to highlightSuggested message to the userSuggested action
Invalid formatsources.email.isFormatValid === falseEmail"The email address provided does not look valid. Please review it."Guide (let them retry)
Auto-correction availablesources.email.emailCorrection is a non-empty stringEmail"Did you mean {emailCorrection}? Please confirm your email address." (substitute the returned value)Guide (offer the fix)
Mailbox does not exist / undeliverablesources.email.isDeliverable === "no"Email"We could not confirm this email address with the provider. Could you double-check it?"Guide (let them retry)
Disposable or temporary emailsources.email.emailType === "disposable"Email"Temporary or disposable email addresses are not accepted. Please use a permanent email address."Guide (require a different address)
Role address (info@, support@, sales@)sources.email.addressSignals.isRoleAddress === trueEmail"This looks like a shared inbox. For your account, please use a personal email address."Guide (soft, tune to your policy)
No-reply addresssources.email.addressSignals.isNoReply === trueEmail"This address cannot receive replies. Please use an email address you can check."Guide (require a different address)
Deliverability unknownsources.email.isDeliverable === "unknown"EmailNo message by default. Allow, and rely on the overall score.Allow (or step up if score is high)
Why isDeliverable === "no" is worth a retry prompt

An undeliverable result is one of the most common ways a legitimate user gets caught. People fat-finger their address (jhon@copany.com), drop a letter, or use an old inbox that no longer exists. Prompting them to review the field recovers the sale or the signup instead of silently rejecting it. Disposable, undeliverable, missing-MX, and no-reply conditions each hold the email risk to at least high, so these are exactly the cases a naive "block on high" rule would reject outright.

IP and network situations

Only act on network signals when the overall risk is elevated

Network signals are context, not a verdict on their own. Plenty of legitimate users are on a VPN, a corporate proxy, or a shared cloud egress. Only surface the network messages below when the top-level level is high or highest. When the overall level is medium or lower, ignore the connection type even if it is vpn, tor, or cloud-provider, and let the submission through. This keeps you from turning away privacy-conscious real users whose connection was the only unusual thing about them.

SituationHow to detect it (exact attributes)Field to highlightSuggested message to the userSuggested action
Tor networklevel is high or highest and sources.ip.connectionType === "tor"Connection (page-level, not a form field)"The connection you are using was flagged as high risk. Please switch to your regular internet connection to continue."Guide (they can switch networks)
Open proxylevel is high or highest and sources.ip.connectionType === "open-proxy"Connection"We could not verify your network connection. Please switch to a standard connection and try again."Guide or step up
VPNlevel is high or highest and sources.ip.connectionType === "vpn"Connection"You appear to be on a VPN. If registration fails, try turning it off and submitting again."Allow with a soft hint, or step up
Datacenter / cloud IPlevel is high or highest and sources.ip.connectionType === "cloud-provider"Connection"We could not verify your connection. Please try again from a standard home or office network."Step up (many real users share cloud egress, so avoid a hard block)
Blocklisted IPlevel is high or highest and sources.ip.blocklisted.isBlockListed === true (check activeReports for severity)Connection"Your connection was flagged for suspicious activity. Please try a different network or contact support."Block or step up
Network messages target the page, not a field

Unlike email problems, network problems are not tied to a single input the user can edit. Surface these as a page-level or form-level notice rather than a red outline on the email box. The message should tell the user what to change (switch networks, disable a VPN) rather than just that they were blocked.

Reading the overall score

After you handle the specific, fixable signals above, use the top-level level as the safety net for everything you did not explicitly check:

levelSuggested default for a signup
lowest, lowAllow
mediumAllow, and optionally log for review
highStep up (email confirmation, extra verification) if no fixable signal explained it
highestBlock or require manual review, per your policy

Putting it together: the decision flow

The order matters. Check the specific, fixable signals first so the user gets a precise message, and fall back to the score only for everything else.

Decide your failure policy up front

This flow assumes a successful scoring call. How you handle a failed or timed-out call is a security decision only you can make, based on your risk tolerance and the sensitivity of the action. Common approaches are a short-timeout retry, queuing the submission for manual review, falling back to your existing validation, or requiring a step-up verification. Whatever you choose, make it explicit rather than letting a failure silently allow or silently block.

  1. Did the request succeed? If not, apply your own failure policy. Fraud scoring should not silently disappear when the API is unreachable, so decide deliberately how a degraded call is handled: retry with a short timeout, queue the submission for review, fall back to your existing validation, or apply a stricter step-up. Choose the path that matches your risk tolerance rather than blanket-allowing or blanket-blocking.
  2. Is there an email correction? Offer it before anything else. It is the friendliest possible outcome.
  3. Is the email invalid, undeliverable, disposable, or a no-reply address? Show the matching field message and let the user correct it. Email guidance runs regardless of the overall score.
  4. Only if the overall level is high or highest: is the network Tor, an open proxy, blocklisted, or another flagged connection type? Show the connection message. Skip this entirely when the score is medium or lower.
  5. Otherwise, look at the top-level level and apply your allow / step-up / block policy.

Here is the same logic as language-agnostic pseudocode. It reads the exact attributes from the response and returns a decision plus a user-facing message.

function decide(response):
email = response.sources.email # may be absent if no email was sent
ip = response.sources.ip # may be absent if no userIp was sent

# 1. Friendliest case first: we have a confident correction.
# Email guidance runs regardless of the overall score.
if email exists and email.emailCorrection is not empty:
return GUIDE(field="email",
message="Did you mean " + email.emailCorrection + "? Please confirm your email address.")

# 2. Fixable email problems, most specific message wins. Shown regardless of score.
if email exists:
if email.isFormatValid == false:
return GUIDE(field="email",
message="The email address provided does not look valid. Please review it.")
if email.emailType == "disposable":
return GUIDE(field="email",
message="Temporary or disposable email addresses are not accepted. Please use a permanent email address.")
if email.addressSignals.isNoReply == true:
return GUIDE(field="email",
message="This address cannot receive replies. Please use an email address you can check.")
if email.isDeliverable == "no": # note: string, not boolean
return GUIDE(field="email",
message="We could not confirm this email address with the provider. Could you double-check it?")
if email.addressSignals.isRoleAddress == true:
return GUIDE(field="email",
message="This looks like a shared inbox. For your account, please use a personal email address.")

# 3. Network problems: ONLY act on these when the overall risk is elevated.
# On medium-or-lower submissions, ignore the connection type entirely.
if ip exists and (response.level == "high" or response.level == "highest"):
if ip.connectionType == "tor" or ip.connectionType == "open-proxy":
return GUIDE(field="connection",
message="The connection you are using was flagged as high risk. Please switch to your regular internet connection to continue.")
if ip.blocklisted.isBlockListed == true:
return STEP_UP(field="connection",
message="Your connection was flagged for suspicious activity. Please try a different network or contact support.")

# 4. Safety net: fall back to the overall risk level.
if response.level == "highest":
return STEP_UP(message="We need an extra step to verify this signup.")
if response.level == "high":
return STEP_UP(message="We need an extra step to verify this signup.")

return ALLOW()

GUIDE, STEP_UP, and ALLOW are your own outcomes. GUIDE re-renders the form with a message on the named field. STEP_UP sends the user through an extra check such as an email confirmation link or a one-time code. ALLOW completes the signup.

Two tracks: email always, network only when risk is elevated

Resolve the precise, fixable email signals first (correction, format, deliverability, disposable, no-reply). These run on every submission regardless of score. Only then, and only when the overall level is high or highest, consider the network signals. Falling back to the raw level is the last step. Checking the score first would collapse every fixable case into a generic "high risk" rejection and you would lose the chance to help.


Worked examples

These use real response shapes from the API.

A mistyped email that can be corrected

The user types jane.doe@gmial.com. The API is confident about the typo:

{
"score": 420,
"level": "medium",
"sources": {
"email": {
"emailAddress": "jane.doe@gmial.com",
"emailCorrection": "jane.doe@gmail.com",
"emailAutoCorrectedFrom": "jane.doe@gmial.com",
"isFormatValid": true,
"isDeliverable": "unknown"
}
}
}

Because emailCorrection is non-empty, you show: "Did you mean jane.doe@gmail.com? Please confirm your email address." One tap and the legitimate user is through. A blanket score rule might have flagged this for review; the correction path recovers it instantly.

An address the provider does not recognize

{
"score": 640,
"level": "high",
"sources": {
"email": {
"emailAddress": "j.doe@example.com",
"emailType": "free",
"isDeliverable": "no",
"isFormatValid": true,
"emailCorrection": ""
}
}
}

isDeliverable is the string "no", so you show the review prompt on the email field instead of rejecting the signup. If the mailbox truly does not exist, the user fixes it. If they abandon, you avoided a guaranteed bounce.

A disposable email

{
"score": 720,
"level": "high",
"factors": ["email:disposable-domain"],
"sources": {
"email": { "emailType": "disposable", "isDeliverable": "no", "isFormatValid": true }
}
}

emailType is "disposable", so you ask for a permanent address. This is a clear, honest message, not a silent block, and genuine users who reached for a throwaway inbox will switch.

A high-risk network

{
"score": 830,
"level": "highest",
"factors": ["ip:tor-exit-node"],
"sources": {
"ip": { "ipAddress": "198.51.100.10", "connectionType": "tor", "blocklisted": { "isBlockListed": false } }
}
}

connectionType is "tor" and the overall level is highest, so both conditions are met and you show the connection notice asking the user to switch to their regular network. A privacy-conscious real user can act on this. Note the gate matters here: if this same tor connection had come back at medium or lower, you would let it through and show no network message at all.


Writing good messages

The messages in this guide follow a few rules that keep them helpful instead of alarming:

  • Say what is wrong and what to do next. "Please review your email address" beats "Invalid input."
  • Point at the right place. Email problems highlight the email field. Network problems appear as a form-level notice.
  • Stay calm and neutral. Avoid accusatory language. Most people who see these messages are legitimate.
  • Never reveal your scoring logic. Do not show the raw score, the factor codes, or "you were flagged as fraud." Tell the user what to fix, nothing more.
  • Keep punctuation simple. Short sentences with commas and periods read as human and clear.

A note on defense in depth

Field-level guidance improves the experience for legitimate users, and it is not a substitute for a firm policy on the genuinely abusive end of the range. Keep a real threshold (for example, step up or hold highest submissions), and let the friendly guidance handle the large middle ground where honest mistakes live.

For the complete list of attributes, values, and factor codes, see: