Account

API documentation / Recipes

Recipes against abuse

Each recipe answers one question a shop or a SaaS asks: how to stop one person from taking the same benefit again under new emails. All of them rest on two things the API returns: the device ID and the number of your accounts seen on that device in the last 30 days.

The recipes

Before you start

  1. Put the collector and the hidden field on the form, as in the Quick start.
  2. Add the helper below to your server code. Besides the form field, the IP address and the headers, it sends user_ref: a hash of the visitor's email. That lets SpoofLens count accounts per device, and the email itself never leaves your server.
  3. Watch before you act. For a week, call the API and keep the answers without acting on them: the request log in your account shows each one with its reasons. Then switch the steps on.
  4. Never decide on an error. If SpoofLens does not answer, the helper returns nothing and the visitor goes on as usual.

The helper

Every recipe calls these two functions. The key comes from the SPOOFLENS_KEY environment variable.

PHP

<?php
// spooflens.php: one call to SpoofLens per form. Returns the answer, or null when SpoofLens does
// not answer in time or answers with an error: then let the visitor through as usual.
function sl_check_visitor(string $userRef): ?array
{
    $ctx = stream_context_create(['http' => [
        'method' => 'POST',
        'header' => "Authorization: Bearer " . getenv('SPOOFLENS_KEY') . "\r\nContent-Type: application/json\r\n",
        'content' => json_encode([
            'blob' => $_POST['sl_blob'] ?? '',
            'ip' => $_SERVER['REMOTE_ADDR'],   // behind Cloudflare: $_SERVER['HTTP_CF_CONNECTING_IP']
            'headers' => [
                'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
                'accept_language' => $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '',
                'sec_ch_ua' => $_SERVER['HTTP_SEC_CH_UA'] ?? '',
                'sec_ch_ua_platform' => $_SERVER['HTTP_SEC_CH_UA_PLATFORM'] ?? '',
                'sec_ch_ua_mobile' => $_SERVER['HTTP_SEC_CH_UA_MOBILE'] ?? '',
            ],
            'user_ref' => $userRef,
        ]),
        'timeout' => 3,
        'ignore_errors' => true,
    ]]);
    $r = json_decode((string)@file_get_contents('https://spooflens.com/v1/assess', false, $ctx), true);
    return is_array($r) && !empty($r['ok']) ? $r : null;
}

// user_ref: a hash of the normalized email, so the email itself never leaves your server
function sl_user_ref(string $email): string
{
    return hash('sha256', strtolower(trim($email)));
}

Node.js

// spooflens.js: one call to SpoofLens per form (Node.js 18+, Express). Resolves to the answer, or to null
// when SpoofLens does not answer in time or answers with an error: then let the visitor through as usual.
const crypto = require('crypto');

async function slCheckVisitor(req, userRef) {
  try {
    const res = await fetch('https://spooflens.com/v1/assess', {
      method: 'POST',
      headers: { Authorization: 'Bearer ' + process.env.SPOOFLENS_KEY, 'Content-Type': 'application/json' },
      body: JSON.stringify({
        blob: (req.body && req.body.sl_blob) || '',
        ip: req.ip,                                  // behind a proxy: app.set('trust proxy', 1)
        headers: {
          user_agent: req.get('user-agent') || '',
          accept_language: req.get('accept-language') || '',
          sec_ch_ua: req.get('sec-ch-ua') || '',
          sec_ch_ua_platform: req.get('sec-ch-ua-platform') || '',
          sec_ch_ua_mobile: req.get('sec-ch-ua-mobile') || '',
        },
        user_ref: userRef,
      }),
      signal: AbortSignal.timeout(3000),
    });
    const r = await res.json();
    return r && r.ok ? r : null;
  } catch (e) {
    return null;
  }
}

// user_ref: a hash of the normalized email, so the email itself never leaves your server
const slUserRef = (email) => crypto.createHash('sha256').update(String(email).trim().toLowerCase()).digest('hex');

module.exports = { slCheckVisitor, slUserRef };

Python

# spooflens.py: one call to SpoofLens per form (Flask + requests). Returns the answer, or None when
# SpoofLens does not answer in time or answers with an error: then let the visitor through as usual.
import hashlib
import os

import requests


def sl_check_visitor(request, user_ref):
    try:
        r = requests.post('https://spooflens.com/v1/assess', timeout=3,
            headers={'Authorization': 'Bearer ' + os.environ['SPOOFLENS_KEY']},
            json={
                'blob': request.form.get('sl_blob', ''),
                'ip': request.remote_addr,          # behind a proxy: werkzeug ProxyFix
                'headers': {
                    'user_agent': request.headers.get('User-Agent', ''),
                    'accept_language': request.headers.get('Accept-Language', ''),
                    'sec_ch_ua': request.headers.get('Sec-CH-UA', ''),
                    'sec_ch_ua_platform': request.headers.get('Sec-CH-UA-Platform', ''),
                    'sec_ch_ua_mobile': request.headers.get('Sec-CH-UA-Mobile', ''),
                },
                'user_ref': user_ref,
            }).json()
    except (requests.RequestException, ValueError):
        return None
    return r if isinstance(r, dict) and r.get('ok') else None


def sl_user_ref(email):
    """A hash of the normalized email, so the email itself never leaves your server."""
    return hashlib.sha256(email.strip().lower().encode()).hexdigest()

What every recipe has in common

Get a free key 1,000 checks a month free, no card.