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.
- 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:
| Outcome | What it means | What the user experiences |
|---|---|---|
| Allow | No blocking signal, acceptable risk | The submission goes through normally |
| Guide | The problem is something the user can fix | A clear, friendly message on the specific field, and a chance to correct it |
| Block or step up | Unrecoverable or clearly abusive | A 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.
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"
}
emailanduserIpare the two fields this guide relies on. At least one of them is required.submissionTypeis 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:
sources.emailis present only when you send anemail.sources.ipis present only when you send auserIp. Always check that a source exists before reading its fields.isDeliverableis 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:
level | score range |
|---|---|
lowest | 200 to 300 |
low | 301 to 400 |
medium | 401 to 600 |
high | 601 to 800 |
highest | 801 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.
| Situation | How to detect it (exact attributes) | Field to highlight | Suggested message to the user | Suggested action |
|---|---|---|---|---|
| Invalid format | sources.email.isFormatValid === false | "The email address provided does not look valid. Please review it." | Guide (let them retry) | |
| Auto-correction available | sources.email.emailCorrection is a non-empty string | "Did you mean {emailCorrection}? Please confirm your email address." (substitute the returned value) | Guide (offer the fix) | |
| Mailbox does not exist / undeliverable | sources.email.isDeliverable === "no" | "We could not confirm this email address with the provider. Could you double-check it?" | Guide (let them retry) | |
| Disposable or temporary email | sources.email.emailType === "disposable" | "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 === true | "This looks like a shared inbox. For your account, please use a personal email address." | Guide (soft, tune to your policy) | |
| No-reply address | sources.email.addressSignals.isNoReply === true | "This address cannot receive replies. Please use an email address you can check." | Guide (require a different address) | |
| Deliverability unknown | sources.email.isDeliverable === "unknown" | No message by default. Allow, and rely on the overall score. | Allow (or step up if score is high) |
isDeliverable === "no" is worth a retry promptAn 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
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.
| Situation | How to detect it (exact attributes) | Field to highlight | Suggested message to the user | Suggested action |
|---|---|---|---|---|
| Tor network | level 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 proxy | level 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 |
| VPN | level 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 IP | level 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 IP | level 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 |
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:
level | Suggested default for a signup |
|---|---|
lowest, low | Allow |
medium | Allow, and optionally log for review |
high | Step up (email confirmation, extra verification) if no fixable signal explained it |
highest | Block 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.
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.
- 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.
- Is there an email correction? Offer it before anything else. It is the friendliest possible outcome.
- 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.
- Only if the overall
levelishighorhighest: is the network Tor, an open proxy, blocklisted, or another flagged connection type? Show the connection message. Skip this entirely when the score ismediumor lower. - Otherwise, look at the top-level
leveland 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.
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:
- Analyze endpoint reference for the full request and response schema.
- Assessed Signals for the categories of signals Opportify evaluates.
- Glossary for definitions of terms used across the docs.