Skip to main content

Add Fraud Protection to an Astro Website

This guide walks you through installing Opportify Form Fraud Protection on an Astro website. Once set up, every form submission is analyzed for fraud signals in real-time: bots, disposable emails, VPNs, and more.

Astro's HTML-first model is a great fit for Opportify: because your forms render to plain HTML, you can lean on the script's automatic interception and let it handle submission for you — no manual fetch wiring required.


Prerequisites

  • Access to the Opportify Admin Console
  • An Astro project (v4+) you can edit
  • Basic familiarity with Astro layouts, components, and .astro files

How it works

Opportify's approach has two parts:

  1. The tracking script — loaded once in the page <head>. It silently observes browser signals and, when a form's action points to an Opportify endpoint, intercepts the submission, injects a per-session risk token, and proxies the request to the Intel API.
  2. The submit endpoint — your form's action points directly to https://form.opportify.ai/intel/v1/submit/<endpoint-id>. Opportify analyzes the submission, stores the result, and (in replace-success mode) reveals a success element for you.

Because Astro renders static HTML, the recommended integration uses the script's declarative attributes (data-opty-mode) rather than a hand-written submit handler. A manual fetch fallback is documented in Step 4 for projects that need full control.


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-site.pages.dev enter my-site.pages.dev

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://form.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

The script must be loaded once, globally, on every page. In Astro the natural place is your shared layout — usually src/layouts/BaseLayout.astro (or whatever layout wraps your pages). Add the tag inside <head> and replace YOUR_PUBLIC_KEY with the key shown in the Opportify Admin Console.

---
// src/layouts/BaseLayout.astro
interface Props {
title: string;
description: string;
}

const { title, description } = Astro.props;
---

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{title}</title>
<meta name="description" content={description} />

<!-- Opportify Fraud Protection — load before first user interaction -->
<script
src="https://cdn.opportify.ai/f/v1.3.8.min.js"
data-opportify-key="YOUR_PUBLIC_KEY"
async
></script>
</head>
<body>
<slot />
</body>
</html>
Astro processes <script> tags — use is:inline for third-party tags

By default Astro bundles and hoists <script> tags it finds in .astro files. A remote, attribute-configured tag like Opportify's must be emitted verbatim. Adding the src attribute keeps it as-is, but if Astro ever rewrites it (or you need inline behavior), add the is:inline directive to opt out of processing:

<script
is:inline
src="https://cdn.opportify.ai/f/v1.3.8.min.js"
data-opportify-key="YOUR_PUBLIC_KEY"
async
></script>
Where to find your public key

In the Opportify Admin Console, go to Settings → API Keys. The public key starts with pk_.


Step 3 — Point your form at the Opportify endpoint

Set the form's action to the Submit URL you copied in Step 1 and use method="POST". When the script sees an action on an Opportify origin, it automatically intercepts the submission — you do not need a custom fetch.

Add data-opty-mode="replace-success" and point data-opty-replace-success at a success element to hide the form and reveal a confirmation message on success.

---
// src/components/ContactForm.astro
const formEndpoint =
"https://form.opportify.ai/intel/v1/submit/YOUR_ENDPOINT_ID";
---

<form
id="contact-form"
action={formEndpoint}
method="POST"
data-opty-mode="replace-success"
data-opty-replace-success="#form-success"
>
<label for="name">Name</label>
<input type="text" id="name" name="name" required minlength="2" />

<label for="email">Email</label>
<input type="email" id="email" name="email" required />

<label for="message">Message</label>
<textarea id="message" name="message" required rows="5"></textarea>

<button type="submit">Send</button>

<p id="form-error" hidden role="alert">Something went wrong. Please try again.</p>
</form>

<!-- Success message lives OUTSIDE the form so replace-success can reveal it -->
<div id="form-success" style="display:none" role="status">
Your message was sent successfully.
</div>

Replace YOUR_ENDPOINT_ID with the UUID from the Submit URL you copied in Step 1.

Why the success element is outside the form

In replace-success mode the script hides the whole form and reveals the element matched by data-opty-replace-success. If that element lived inside the form, it would be hidden too. Keep it as a sibling. See the Configuration Reference for the full list of modes (inline, redirect, replace-success).


Step 4 — (Optional) Manual fetch fallback

Astro's automatic interception covers most cases. This path is only for custom client-side form handling — for example, running your own validation, or keeping the form working when the script is blocked (ad-blocker, offline test env). It is not a place to reimplement bot or fraud heuristics: Opportify performs all bot, automation, and honeypot detection server-side regardless of which submission path is used. If you take this route, add a client script that:

  1. Runs your own validation first.
  2. Detects whether Opportify instrumented the form (it sets data-op-init="1").
  3. If active, lets the script submit; if not, POSTs the payload as JSON itself.

Astro runs <script> blocks in .astro files on the client, so place this alongside your form component.

<script>
const form = document.getElementById("contact-form") as HTMLFormElement | null;
const successMsg = document.getElementById("form-success");
const errorMsg = document.getElementById("form-error");

if (form) {
// The Opportify script sets data-op-init="1" once it instruments a form.
const isOpportifyActive = () => form.getAttribute("data-op-init") === "1";

form.addEventListener(
"submit",
async (e) => {
errorMsg?.setAttribute("hidden", "");

// Basic client-side validation.
if (!form.checkValidity()) {
e.preventDefault();
e.stopImmediatePropagation();
form.reportValidity();
return;
}

// Opportify active: let its handler POST as JSON and reveal #form-success.
if (isOpportifyActive()) return;

// Fallback: script not loaded — submit directly as JSON.
e.preventDefault();

const formData = new FormData(form);
const payload: Record<string, string> = {};
formData.forEach((value, key) => {
payload[key] = value as string;
});

try {
const response = await fetch(form.action, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(payload),
});

if (response.ok) {
form.style.display = "none";
if (successMsg) successMsg.style.display = "";
} else {
errorMsg?.removeAttribute("hidden");
}
} catch {
errorMsg?.removeAttribute("hidden");
}
},
true, // capture: run before the Opportify SDK's bubble-phase handler
);
}
</script>
When to use this

Skip this step if the declarative data-opty-mode setup in Step 3 already does what you need. Add it only when you require custom validation or a no-JavaScript-script fallback path.

Reading the injected tokens manually

If you build your payload by hand instead of using FormData, remember the script injects two hidden inputs into the form — opportifyToken (per-session risk token) and opportifyFormUUID (the matched endpoint). Read them with document.querySelector('input[name="opportifyToken"]') and include both in the JSON body.


Step 5 — Configure your Content Security Policy

If your Astro site ships a Content Security Policy (common for statically hosted sites on Vercel, Netlify, or Cloudflare Pages via a _headers file or vercel.json), allowlist the Opportify origins:

Content-Security-Policy:
script-src 'self' https://cdn.opportify.ai;
connect-src 'self' https://api.opportify.ai https://form.opportify.ai;
frame-src 'self' https://cdn.opportify.ai;
form-action 'self' https://form.opportify.ai;

Append these to your existing directives — do not replace them. See the CSP guide for the full breakdown of each directive.

Inline scripts in Astro + strict CSP

If you use a strict script-src without 'unsafe-inline', any is:inline script (including a loader you write yourself) needs a matching sha256-... hash or a nonce in script-src. The Opportify tag loaded via src only needs https://cdn.opportify.ai allowlisted — no hash required.


Step 6 — Setup is complete

Your Astro 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 in the Opportify Admin Console. For each submission you can see:

FieldDescription
Risk ScoreNumeric fraud risk score (200–1000) — higher means more suspicious
Risk LevelLowest / Low / Medium / High / Highest — colour-coded for quick triage
IP AddressThe originating IP of the submission
CountryGeo-location derived from the IP
EmailThe email address submitted, if collected
Submitted AtTimestamp of when the submission was received
Form EndpointWhich endpoint (and therefore which form) received the submission
Fraud SignalsIndividual signals that fired (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

SymptomLikely causeFix
Script never runs / data-op-init never setAstro bundled or dropped the tagLoad it via src in <head>, and add is:inline if Astro rewrites it (Step 2)
CSP error in browser consoleMissing CSP directives for Opportify originsAdd https://cdn.opportify.ai to script-src and https://api.opportify.ai / https://form.opportify.ai to connect-src. See the CSP guide.
is:inline loader blocked by CSPStrict script-src without a matching hash/nonceAdd the script's sha256-... hash (or a nonce) to script-src, or load via src instead of inline
Form submits twiceManual handler and auto-interception both firingUse capture phase + data-op-init detection (Step 4), or drop the manual handler entirely
Success message stays hiddenSuccess element is inside the <form>Move #form-success outside the form so replace-success can reveal it (Step 3)
404 on submitWrong endpoint ID in the actionDouble-check the Submit URL copied from the Opportify Admin Console