Skip to main content
< All Topics
Print

How to Integrate Spamvora API on Any Website

The WordPress plugin handles everything automatically. But if you’re running a React app, a Next.js site, a plain HTML page, a Django project, or any custom stack, you can still use Spamvora’s spam detection — you just wire it up yourself.

This guide walks you through exactly how the API works, based directly on the plugin’s source code, and shows you how to replicate it in your own stack.

How the API Works — The Big Picture

Before writing any code, understand the two-step flow:

How to Integrate Spamvora API on Any Website

Two endpoints. One token. One spam verdict. That’s it.

What You Need

  • A Spamvora account at app.spamvora.com
  • Your Site Key from the Spamvora dashboard (looks like site_live_xxxxxxxxx)
  • A server-side language (Node.js, Python, PHP, anything)

Step 1 — Load the Tracker Script (Front-end)

The tracker runs silently in the background and collects three signals that the spam detection uses:

  • visitor_id — a persistent UUID per browser (stored in localStorage)
  • traffic_source — where the visitor came from: referrer, UTM params, landing page
  • navigation_path — which pages they visited in this session and for how long

Option A — Self-host the script

Download tracker.min.js from the plugin ZIP and serve it from your own domain. Load it on every page:

<script>
  window.SP_SITE_ID = "your_site_id_here"; // any unique string for your site
</script>
<script src="/js/tracker.min.js"></script>

What is SP_SITE_ID? It’s a namespace for the localStorage/sessionStorage keys. Set it to any consistent string (your domain slug works fine, e.g. "mysite"). It prevents key collisions if someone has multiple Spamvora-protected sites open.

Option B — Use a CDN or inline it

You can also inline the tracker script directly into your HTML <head> for zero extra requests.

Step 2 — Mark the Forms You Want Protected (Front-end)

The formsentinel.js script only activates on forms that have the class sv-protected. Add it to any form you want to protect:

<form id="contact-form" class="sv-protected" method="POST" action="/submit">
  <input type="text" name="name" placeholder="Your name" />
  <input type="email" name="email" placeholder="Email" />
  <textarea name="message" placeholder="Message"></textarea>
  <button type="submit">Send</button>
</form>

Step 3 — Collect and Inject Form Data on Submit (Front-end)

When the visitor clicks Submit, you need to collect two things and attach them as a hidden JSON field (__fs_meta) before the form POST reaches your server.

Load formsentinel.js (from the plugin ZIP) after your form:

<script src="/js/tracker.min.js"></script>
<script src="/js/formsentinel.js"></script>

That’s all you need on the front-end side. The script listens for form submits in capture phase, and for any form with class="sv-protected" it:

  1. Collects all input/textarea/select fields (skipping password and file types)
  2. Reads the browser environment from collectBrowserEnvironment()
  3. Reads visitor tracking data from localStorage/sessionStorage
  4. Injects a hidden __fs_meta input with the combined JSON payload

Your form POST will now include a hidden field __fs_meta containing:

{
  "f": [
    {
      "name": "email",
      "field_type": "email",
      "value": "test@example.com",
      "length": 17,
      "required": false
    }
  ],
  "environment": {
    "u": {
      "visitor_id": "uuid-from-localstorage",
      "traffic_source": {
        "referrer": "https://google.com",
        "utm_source": "google",
        "utm_medium": "cpc",
        "utm_campaign": null,
        "landing_page": "https://yoursite.com/contact",
        "timestamp": 1751000000000
      },
      "navigation_path": [
        { "step": 1, "page": "/", "enter": 1751000000000, "leave": 1751000060000 },
        { "step": 2, "page": "/contact", "enter": 1751000060000, "leave": null }
      ]
    },
    "b": {
      "user_agent": "Mozilla/5.0 ...",
      "browser_name": "Chrome",
      "browser_version": "124.0.0.0",
      "os_name": "Windows",
      "os_version": "",
      "platform": "Windows",
      "device_type": "desktop",
      "screen_width": 1920,
      "screen_height": 1080,
      "viewport_width": 1440,
      "viewport_height": 900,
      "color_depth": 24,
      "device_pixel_ratio": 1,
      "timezone": "Asia/Kolkata",
      "timezone_offset": -330,
      "language": "en-IN",
      "languages": ["en-IN", "en"]
    }
  },
  "crn_url": "https://yoursite.com/contact",
  "timestamp": 1751000120000,
  "nonce": ""
}

Step 4 — Get a Session Token (Server-side)

Every spam check needs a fresh session token. Your server requests one by sending your Site Key and domain to the Spamvora API.

Endpoint: POST https://app.spamvora.com/api/v1/session/

Request body:

{
  "site_key": "site_live_xxxxxxxxx",
  "domain": "yoursite.com"
}

Response:

{
  "session_token": "some-long-token-string"
}

If something is wrong (invalid key, unregistered domain), you’ll get an error field instead of session_token.

Node.js Example

async function getSessionToken(siteKey, domain) {
  const response = await fetch('https://app.spamvora.com/api/v1/session/', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ site_key: siteKey, domain: domain }),
  });

  const data = await response.json();

  if (!data.session_token) {
    throw new Error('Session error: ' + JSON.stringify(data));
  }

  return data.session_token;
}

Python Example

import httpx

def get_session_token(site_key: str, domain: str) -> str:
    resp = httpx.post(
        'https://app.spamvora.com/api/v1/session/',
        json={'site_key': site_key, 'domain': domain},
        timeout=5,
    )
    data = resp.json()

    if 'session_token' not in data:
        raise Exception(f"Session error: {data}")

    return data['session_token']

PHP (vanilla) Example

function spamvora_get_session_token(string $site_key, string $domain): ?string {
    $ch = curl_init('https://app.spamvora.com/api/v1/session/');
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_HTTPHEADER     => ['Content-Type: application/json'],
        CURLOPT_POSTFIELDS     => json_encode([
            'site_key' => $site_key,
            'domain'   => $domain,
        ]),
        CURLOPT_TIMEOUT => 5,
    ]);
    $body = curl_exec($ch);
    curl_close($ch);

    $data = json_decode($body, true);
    return $data['session_token'] ?? null;
}

Step 5 — Send the Spam Check Request (Server-side)

This is the main call. You send the form data to Spamvora along with an HMAC-SHA256 signature so the API can verify the request wasn’t tampered with.

Endpoint: POST https://app.spamvora.com/api/v1/spam-check/

Required headers:

HeaderValue
Content-Typeapplication/json
X-SITE-KEYYour site key
X-SESSION-TOKENToken from Step 4
X-SIGNATUREHMAC-SHA256 of the raw JSON body, signed with the session token

How the signature works:

signature = HMAC-SHA256(raw_json_body, session_token)

The body and the signature must match exactly — do not pretty-print the JSON or the signature will be wrong.

Request body structure:

{
  "form_fields": [ ... ],      // the "f" array from __fs_meta
  "ip": "123.45.67.89",        // visitor's real IP (detect server-side)
  "environment": { ... },      // the "environment" object from __fs_meta
  "crn_url": "https://yoursite.com/contact",
  "timestamp": 1751000120,     // Unix timestamp in SECONDS (not ms)
  "nonce": "a3f8c2d1e9b04f76..." // 32 random hex chars
}

Note on timestamp: The plugin uses time() (PHP seconds). Use seconds, not milliseconds.

Note on IP: The client-side __fs_meta sends ip: "" (empty). Your server must fill in the real visitor IP. Get it from the correct request header depending on your setup (see the IP detection section below).

Node.js / Express — Full Handler

const crypto = require('crypto');
const express = require('express');
const app = express();
app.use(express.urlencoded({ extended: true }));

const SITE_KEY = process.env.SPAMVORA_SITE_KEY;
const DOMAIN   = 'yoursite.com';

function getVisitorIp(req) {
  // Cloudflare
  if (req.headers['cf-connecting-ip']) return req.headers['cf-connecting-ip'];
  // Nginx/reverse proxy
  if (req.headers['x-real-ip']) return req.headers['x-real-ip'];
  // Proxy chain (take first public IP)
  if (req.headers['x-forwarded-for']) {
    return req.headers['x-forwarded-for'].split(',')[0].trim();
  }
  return req.socket.remoteAddress;
}

app.post('/submit', async (req, res) => {
  const rawMeta = req.body.__fs_meta;

  if (!rawMeta) {
    // __fs_meta not present — formsentinel.js didn't run
    // Decide: block or allow. Blocking is safer.
    return res.status(400).send('Bad request.');
  }

  let meta;
  try {
    meta = JSON.parse(rawMeta);
  } catch {
    return res.status(400).send('Invalid meta.');
  }

  const visitorIp = getVisitorIp(req);

  // ── Step 4: Get session token ──────────────────────────────────────
  let sessionToken;
  try {
    const sessionResp = await fetch('https://app.spamvora.com/api/v1/session/', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ site_key: SITE_KEY, domain: DOMAIN }),
    });
    const sessionData = await sessionResp.json();
    if (!sessionData.session_token) throw new Error(JSON.stringify(sessionData));
    sessionToken = sessionData.session_token;
  } catch (err) {
    console.error('Spamvora session error:', err);
    // Fail open: allow the submission if Spamvora is unreachable
    return handleCleanSubmission(req, res, meta);
  }

  // ── Step 5: Build and sign the spam check payload ─────────────────
  const nonce   = crypto.randomBytes(16).toString('hex');
  const payload = {
    form_fields: meta.f || [],
    ip:          visitorIp,
    environment: meta.environment || {},
    crn_url:     meta.crn_url || '',
    timestamp:   Math.floor(Date.now() / 1000),   // seconds
    nonce:       nonce,
  };

  const rawBody  = JSON.stringify(payload);        // must be exact bytes signed
  const sig      = crypto.createHmac('sha256', sessionToken)
                         .update(rawBody)
                         .digest('hex');

  let result;
  try {
    const spamResp = await fetch('https://app.spamvora.com/api/v1/spam-check/', {
      method: 'POST',
      headers: {
        'Content-Type':    'application/json',
        'X-SITE-KEY':      SITE_KEY,
        'X-SESSION-TOKEN': sessionToken,
        'X-SIGNATURE':     sig,
      },
      body: rawBody,
    });
    result = await spamResp.json();
  } catch (err) {
    console.error('Spamvora check error:', err);
    return handleCleanSubmission(req, res, meta); // fail open
  }

  // ── Step 6: Act on the result ─────────────────────────────────────
  if (result.is_spam) {
    return res.status(200).json({ success: false, message: 'Spam detected.' });
  }

  return handleCleanSubmission(req, res, meta);
});

function handleCleanSubmission(req, res, meta) {
  // Your real form processing logic here
  // Send email, save to DB, etc.
  return res.status(200).json({ success: true, message: 'Thank you!' });
}

app.listen(3000);

Python / Flask — Full Handler

import hashlib
import hmac
import json
import os
import secrets
import time

import httpx
from flask import Flask, jsonify, request

app = Flask(__name__)

SITE_KEY = os.environ['SPAMVORA_SITE_KEY']
DOMAIN   = 'yoursite.com'
API_BASE = 'https://app.spamvora.com/api/v1/'


def get_visitor_ip() -> str:
    """Pick the real public IP from request headers."""
    for header in [
        'CF-Connecting-IP',     # Cloudflare
        'X-Real-IP',            # Nginx
        'X-Forwarded-For',      # Proxy chain
    ]:
        value = request.headers.get(header)
        if value:
            return value.split(',')[0].strip()
    return request.remote_addr or '0.0.0.0'


@app.post('/submit')
def submit():
    raw_meta = request.form.get('__fs_meta')

    if not raw_meta:
        return jsonify(success=False, message='Bad request.'), 400

    try:
        meta = json.loads(raw_meta)
    except json.JSONDecodeError:
        return jsonify(success=False, message='Invalid meta.'), 400

    visitor_ip = get_visitor_ip()

    # ── Step 4: Get session token ─────────────────────────────────────
    try:
        sess_resp = httpx.post(
            API_BASE + 'session/',
            json={'site_key': SITE_KEY, 'domain': DOMAIN},
            timeout=5,
        )
        sess_data = sess_resp.json()
        session_token = sess_data.get('session_token')
        if not session_token:
            raise ValueError(f"No session token: {sess_data}")
    except Exception as e:
        print(f"Spamvora session error: {e}")
        return handle_clean(meta)   # fail open

    # ── Step 5: Sign and send spam check ─────────────────────────────
    nonce = secrets.token_hex(16)
    payload = {
        'form_fields': meta.get('f', []),
        'ip':          visitor_ip,
        'environment': meta.get('environment', {}),
        'crn_url':     meta.get('crn_url', ''),
        'timestamp':   int(time.time()),          # seconds
        'nonce':       nonce,
    }

    raw_body  = json.dumps(payload, separators=(',', ':'))  # compact JSON
    signature = hmac.new(
        session_token.encode(),
        raw_body.encode(),
        hashlib.sha256
    ).hexdigest()

    try:
        check_resp = httpx.post(
            API_BASE + 'spam-check/',
            content=raw_body,
            headers={
                'Content-Type':    'application/json',
                'X-SITE-KEY':      SITE_KEY,
                'X-SESSION-TOKEN': session_token,
                'X-SIGNATURE':     signature,
            },
            timeout=5,
        )
        result = check_resp.json()
    except Exception as e:
        print(f"Spamvora check error: {e}")
        return handle_clean(meta)   # fail open

    # ── Step 6: Act on result ─────────────────────────────────────────
    if result.get('is_spam'):
        return jsonify(success=False, message='Spam detected.'), 200

    return handle_clean(meta)


def handle_clean(meta):
    # Your real processing: send email, save to DB, etc.
    return jsonify(success=True, message='Thank you!'), 200

PHP (Vanilla) — Full Handler

<?php
define('SPAMVORA_SITE_KEY', getenv('SPAMVORA_SITE_KEY'));
define('SPAMVORA_DOMAIN',   'yoursite.com');
define('SPAMVORA_API_BASE', 'https://app.spamvora.com/api/v1/');

function get_visitor_ip(): string {
    $headers = [
        'HTTP_CF_CONNECTING_IP',  // Cloudflare
        'HTTP_X_REAL_IP',         // Nginx
        'HTTP_X_FORWARDED_FOR',   // Proxy chain
        'REMOTE_ADDR',
    ];
    foreach ($headers as $h) {
        if (!empty($_SERVER[$h])) {
            $ip = trim(explode(',', $_SERVER[$h])[0]);
            if (filter_var($ip, FILTER_VALIDATE_IP,
                FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
                return $ip;
            }
        }
    }
    return '0.0.0.0';
}

function spamvora_post(string $endpoint, string $body, array $headers): ?array {
    $ch = curl_init(SPAMVORA_API_BASE . $endpoint);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => $body,
        CURLOPT_HTTPHEADER     => $headers,
        CURLOPT_TIMEOUT        => 5,
    ]);
    $result = curl_exec($ch);
    curl_close($ch);
    return $result ? json_decode($result, true) : null;
}

// ── Receive form POST ─────────────────────────────────────────────────────────
$raw_meta = $_POST['__fs_meta'] ?? null;

if (!$raw_meta) {
    http_response_code(400);
    echo json_encode(['success' => false, 'message' => 'Bad request.']);
    exit;
}

$meta = json_decode($raw_meta, true);
if (!$meta) {
    http_response_code(400);
    echo json_encode(['success' => false, 'message' => 'Invalid meta.']);
    exit;
}

$visitor_ip = get_visitor_ip();

// ── Step 4: Get session token ─────────────────────────────────────────────────
$sess = spamvora_post('session/', json_encode([
    'site_key' => SPAMVORA_SITE_KEY,
    'domain'   => SPAMVORA_DOMAIN,
]), ['Content-Type: application/json']);

$session_token = $sess['session_token'] ?? null;

if (!$session_token) {
    // Fail open — Spamvora unreachable
    process_clean_submission($meta);
    exit;
}

// ── Step 5: Sign and send spam check ─────────────────────────────────────────
$nonce   = bin2hex(random_bytes(16));
$payload = [
    'form_fields' => $meta['f'] ?? [],
    'ip'          => $visitor_ip,
    'environment' => $meta['environment'] ?? [],
    'crn_url'     => $meta['crn_url'] ?? '',
    'timestamp'   => time(),            // seconds
    'nonce'       => $nonce,
];

$raw_body  = json_encode($payload, JSON_UNESCAPED_SLASHES);
$signature = hash_hmac('sha256', $raw_body, $session_token);

$result = spamvora_post('spam-check/', $raw_body, [
    'Content-Type: application/json',
    'X-SITE-KEY: '      . SPAMVORA_SITE_KEY,
    'X-SESSION-TOKEN: ' . $session_token,
    'X-SIGNATURE: '     . $signature,
]);

// ── Step 6: Act on result ─────────────────────────────────────────────────────
if (!empty($result['is_spam'])) {
    echo json_encode(['success' => false, 'message' => 'Spam detected.']);
    exit;
}

process_clean_submission($meta);

function process_clean_submission(array $meta): void {
    // Your real processing here: send email, save to DB, etc.
    echo json_encode(['success' => true, 'message' => 'Thank you!']);
}

Handling API Errors

The API can return error strings in the error field. Here are the ones the plugin handles:

Error valueWhat it means
Invalid siteYour site key or domain is not registered on Spamvora
session_errorCould not reach the session endpoint (network issue)
session_tokenThe session token was missing or rejected
session_failedThe session request returned but no token was given
server_errorSpamvora API is having issues

Recommended error strategy: If the error is one of the above, treat it as a soft failure — let the submission through (fail open). Only hard-block when is_spam: true comes back cleanly.

// Node.js example
if (result.error && ['Invalid site', 'session_error', 'session_token',
                      'session_failed', 'server_error'].includes(result.error)) {
  // Something is wrong with Spamvora config or connectivity
  // Log it, alert yourself, but don't block real users
  console.error('Spamvora error:', result.error);
  return handleCleanSubmission(req, res, meta);
}

if (result.is_spam) {
  return res.status(200).json({ success: false, message: 'Spam detected.' });
}

Getting the Real Visitor IP

IP reputation is one of the signals Spamvora uses. The wrong IP reduces detection accuracy. Here’s the priority order to detect it correctly:

1. CF-Connecting-IP ← if behind Cloudflare (most trusted)
2. X-Real-IP ← if behind Nginx as reverse proxy
3. X-Forwarded-For ← if behind any other proxy (take first IP in list)
4. REMOTE_ADDR ← direct connection (last resort)

Never trust X-Forwarded-For blindly — it can be spoofed by clients unless your proxy strips it first. If you’re behind Cloudflare, CF-Connecting-IP is the safest source.

Quick Reference

API Base URL: https://app.spamvora.com/api/v1/

Session:
   Method: POST /session/
   Body: { site_key, domain }
   Returns: { session_token }

Spam Check:
   Method: POST /spam-check/
   Headers: X-SITE-KEY, X-SESSION-TOKEN, X-SIGNATURE
   Body: { form_fields, ip, environment, crn_url, timestamp, nonce }
   Returns: { is_spam: true|false } or { error: “…” }

Signature:
   HMAC-SHA256(raw_json_body_string, session_token) → hex string

   Timestamp: Unix seconds (not milliseconds)
   Nonce: 32 hex characters (16 random bytes)

Common Mistakes

Wrong timestamp format — use seconds (Math.floor(Date.now() / 1000)) not milliseconds (Date.now()).

Signing the wrong thing — sign the exact string you send as the body. If you JSON-serialize the payload twice or pretty-print it, the signature will not match. Build the payload once, store it in a variable, then sign and send that same variable.

Not filling the IP — the front-end JS sends ip: "" intentionally. Your server must fill in the real IP before sending to the API.

Forgetting sv-protected classformsentinel.js only activates on forms with this class. Without it, __fs_meta will never be injected.

Blocking on session errors — if Spamvora is temporarily unreachable, blocking every form submission on your site would be worse than letting a few spam submissions through. Fail open on connectivity errors, block only on confirmed is_spam: true.

Table of Contents