Add Fraud Protection to a Meteor.js Website
This guide walks you through installing Opportify Form Fraud Protection on a Meteor.js (v3) website. Once set up, every form submission is analyzed for fraud signals in real-time: bots, disposable emails, VPNs, and more.
Prerequisites
- Access to the Opportify Admin Console
- A Meteor.js v3 project you can edit
- Basic familiarity with Meteor's file structure and either Blaze templates or React components
How it works
Opportify's approach has two parts:
- The tracking script — loaded once in the page
<head>. It silently observes browser signals and injects two hidden values into each of your forms:opportifyToken(a per-session risk token) andopportifyFormUUID(the identifier of the matched form endpoint). - The submit endpoint — instead of POSTing to your own backend or calling a Meteor Method, your form sends data directly to
https://api.opportify.ai/intel/v1/submit/<endpoint-id>. Opportify analyzes the submission, stores the result, and returns a JSON response your app can act on.
Step-by-Step Setup
Step 1 — Open Opportify Admin and complete the Quick Start
Navigate to Quick Start and complete Steps 1 through 3.
Step 1 — Allowlist your domain. Enter your site's hostname (no https://, no trailing slash).
Example: for
https://my-app.meteorapp.comentermy-app.meteorapp.com
Step 2 — Create a Form Endpoint. Click + New Endpoint, give it a descriptive name (e.g. Contact Form), and select a public key. Each endpoint maps to one form on your site.
Step 3 — Copy the Submit URL. From the endpoint list, copy the value in the Submit URL column. It looks like:
https://api.opportify.ai/intel/v1/submit/<your-endpoint-id>
Keep this URL handy — you will use it in Step 3 below.
Step 2 — Load the Opportify script
In Meteor, the client-side HTML shell lives in client/main.html. Add the script tag inside the <head> block. Replace YOUR_PUBLIC_KEY with the key shown in the Opportify Admin Console.
<!-- client/main.html -->
<head>
<title>My App</title>
<!-- Opportify Fraud Protection — load before first user interaction -->
<script
src="https://cdn.opportify.ai/f/v1.3.4.min.js"
data-opportify-key="YOUR_PUBLIC_KEY"
async
></script>
</head>
<body>
<!-- Blaze renders into the body; React apps mount here too -->
</body>
In the Opportify Admin Console, go to Settings → API Keys. The public key starts with pk_.
<html> wrapper neededMeteor automatically assembles the full HTML document from <head> and <body> blocks found anywhere in client/. You only need to provide the content, not a full HTML document.
Step 3 — Connect your form to the Opportify endpoint
Replace the current form submission logic so it POSTs to the Opportify Submit URL instead of calling a Meteor Method or your own endpoint.
Before
// Calling a Meteor Method
Meteor.call('submitContactForm', payload, (err, result) => { ... });
// or fetching your own backend
await fetch('/api/contact-form', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
After
const response = await fetch(
'https://api.opportify.ai/intel/v1/submit/YOUR_ENDPOINT_ID',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
}
);
Replace YOUR_ENDPOINT_ID with the UUID from the Submit URL you copied in Step 1.
Step 4 — Include the Opportify tokens in the payload
When the tracking script runs, it injects two hidden <input> fields into your form:
| Field name | Purpose |
|---|---|
opportifyToken | Per-session risk token generated by the script |
opportifyFormUUID | Identifies which Form Endpoint matched this form |
Read these values from the DOM and include them in the payload before the fetch call:
const opportifyToken =
document
.querySelector('input[name="opportifyToken"]')
?.value ?? '';
const opportifyFormUUID =
document
.querySelector('input[name="opportifyFormUUID"]')
?.value ?? '';
const payload = {
name,
email,
message,
opportifyToken,
opportifyFormUUID,
};
The Opportify script injects opportifyToken and opportifyFormUUID as hidden inputs directly into the rendered HTML form — not into Meteor's reactive data or React state. Reading them with document.querySelector is the correct approach here.
Step 5 — Disable automatic interception on self-managed forms (optional)
By default, the Opportify script intercepts form submissions automatically. If your form already handles its own submit event (as shown in the full examples below), add the data-opty-submit-interception="disabled" attribute to the <form> element to prevent double submission:
<form data-opty-submit-interception="disabled" id="contact-form">
<!-- form fields -->
</form>
Skip this step if you want the script to handle submission automatically (e.g. for simpler forms with no custom submit logic).
Step 6 — Full examples
Choose the rendering approach that matches your Meteor project.
Option A — Blaze template
<!-- client/main.html -->
<head>
<title>My App</title>
<script
src="https://cdn.opportify.ai/f/v1.3.4.min.js"
data-opportify-key="YOUR_PUBLIC_KEY"
async
></script>
</head>
<body>
{{> contactForm}}
</body>
<template name="contactForm">
<form data-opty-submit-interception="disabled" id="contact-form">
<input type="text" name="name" placeholder="Your name" required />
<input type="email" name="email" placeholder="your@email.com" required />
<textarea name="message" placeholder="Your message" required></textarea>
{{#if error}}<p style="color:red">{{error}}</p>{{/if}}
{{#if success}}<p style="color:green">{{success}}</p>{{/if}}
<button type="submit">{{#if submitting}}Sending…{{else}}Send{{/if}}</button>
</form>
</template>
// client/main.js
import { Template } from 'meteor/templating';
import { ReactiveVar } from 'meteor/reactive-var';
import './main.html';
Template.contactForm.onCreated(function () {
this.error = new ReactiveVar('');
this.success = new ReactiveVar('');
this.submitting = new ReactiveVar(false);
});
Template.contactForm.helpers({
error() { return Template.instance().error.get(); },
success() { return Template.instance().success.get(); },
submitting() { return Template.instance().submitting.get(); },
});
Template.contactForm.events({
async 'submit #contact-form'(event, instance) {
event.preventDefault();
instance.submitting.set(true);
instance.error.set('');
const form = event.target;
const opportifyToken =
form.querySelector('input[name="opportifyToken"]')?.value ?? '';
const opportifyFormUUID =
form.querySelector('input[name="opportifyFormUUID"]')?.value ?? '';
const payload = {
name: form.name.value,
email: form.email.value,
message: form.message.value,
opportifyToken,
opportifyFormUUID,
};
try {
const response = await fetch(
'https://api.opportify.ai/intel/v1/submit/YOUR_ENDPOINT_ID',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
}
);
const result = await response.json();
if (!result.accepted) {
instance.error.set(result.errorMessage ?? 'Something went wrong. Please try again.');
} else {
instance.success.set('Your message was sent successfully.');
form.reset();
}
} catch {
instance.error.set('Network error. Please check your connection and try again.');
} finally {
instance.submitting.set(false);
}
},
});
Option B — React component inside Meteor
If your Meteor app uses React (the recommended approach for Meteor v3), the integration mirrors the React guide with one difference: the Opportify script is loaded in client/main.html instead of a _document.tsx or layout.tsx file.
<!-- client/main.html -->
<head>
<title>My App</title>
<!-- Opportify Fraud Protection — load before first user interaction -->
<script
src="https://cdn.opportify.ai/f/v1.3.4.min.js"
data-opportify-key="YOUR_PUBLIC_KEY"
async
></script>
</head>
<body>
<div id="react-target"></div>
</body>
// imports/ui/ContactForm.jsx
import { useState } from 'react';
export function ContactForm() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [message, setMessage] = useState('');
const [successMessage, setSuccessMessage] = useState('');
const [errorMessage, setErrorMessage] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const onSubmit = async (e) => {
e.preventDefault();
setIsSubmitting(true);
setErrorMessage('');
const opportifyToken =
document
.querySelector('input[name="opportifyToken"]')
?.value ?? '';
const opportifyFormUUID =
document
.querySelector('input[name="opportifyFormUUID"]')
?.value ?? '';
const payload = { name, email, message, opportifyToken, opportifyFormUUID };
try {
const response = await fetch(
'https://api.opportify.ai/intel/v1/submit/YOUR_ENDPOINT_ID',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
}
);
const result = await response.json();
if (!result.accepted) {
setErrorMessage(result.errorMessage ?? 'Something went wrong. Please try again.');
} else {
setSuccessMessage('Your message was sent successfully.');
setName(''); setEmail(''); setMessage('');
}
} catch {
setErrorMessage('Network error. Please check your connection and try again.');
} finally {
setIsSubmitting(false);
}
};
return (
<form data-opty-submit-interception="disabled" onSubmit={onSubmit}>
<input value={name} onChange={e => setName(e.target.value)} placeholder="Your name" required />
<input type="email" value={email} onChange={e => setEmail(e.target.value)} placeholder="your@email.com" required />
<textarea value={message} onChange={e => setMessage(e.target.value)} placeholder="Your message" required />
{errorMessage && <p style={{ color: 'red' }}>{errorMessage}</p>}
{successMessage && <p style={{ color: 'green' }}>{successMessage}</p>}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Sending…' : 'Send'}
</button>
</form>
);
}
// client/main.jsx
import React from 'react';
import { Meteor } from 'meteor/meteor';
import { createRoot } from 'react-dom/client';
import { ContactForm } from '../imports/ui/ContactForm';
Meteor.startup(() => {
const root = createRoot(document.getElementById('react-target'));
root.render(<ContactForm />);
});
Step 7 — Setup is complete
Your Meteor.js form is now connected to Opportify Fraud Protection.
Return to the Opportify Admin Console and complete the remaining Quick Start steps to fine-tune your setup:
- Step 4 — Data Retention: Choose how long submission data is kept.
- Alerts: Configure email or in-app alerts for suspicious submissions.
- Webhooks: Forward fraud signals and submission data to your own backend or third-party tools.
Viewing Form Submissions
After deploying, every form submission from your site will appear in the Form Submissions page of the Opportify Admin Console. For each submission you can see:
| Field | Description |
|---|---|
| Risk Score | A numeric fraud risk rating assigned to the submission |
| Risk Level | A human-readable label: Lowest, Low, Medium, High, or Highest |
| IP Address | The originating IP of the submission |
| Country | Geo-location derived from the IP |
| The email address submitted, if collected | |
| Submitted At | Timestamp of when the submission was received |
| Form Endpoint | Which endpoint (and therefore which form) received the submission |
| Fraud Signals | Individual signals that contributed to the risk score (e.g. disposable email, VPN detected, bot behavior) |
You can filter submissions by risk level, date range, or endpoint to quickly identify and act on suspicious activity.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
opportifyToken is always empty | Script not loaded or loaded after form render | Ensure the script tag is in client/main.html <head> with async and the page has fully loaded before submission |
| CSP error in browser console | Missing CSP directives for Opportify origins | Add https://cdn.opportify.ai to script-src and https://api.opportify.ai to connect-src in your Content Security Policy |
| Form submits twice | Script auto-interception active on a self-managed form | Add data-opty-submit-interception="disabled" to the <form> element (Step 5) |
404 on submit | Wrong endpoint ID in the URL | Double-check the Submit URL copied from the Opportify Admin Console |
| Script not loading in production | Meteor bundler stripping unknown script attributes | Confirm data-opportify-key is preserved in your build; check the rendered <head> in your deployed app |