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
- Free trial abuse: one trial per person, not per email
Count the accounts each browser opens and add a step only for the second and later ones. - Welcome coupons: a first-order discount for first orders only
Check the device when a welcome coupon is used, and refuse the discount, not the order. - Referral programs: no bonus for inviting yourself
Compare the device of the new account with the referrer's, and pay the bonus on a first payment only.
Before you start
- Put the collector and the hidden field on the form, as in the Quick start.
- 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. - 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.
- 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
- A step for doubtful cases, not a ban: an honest person who looks doubtful can still get through.
- One check at the moment the benefit is given: the sign-up, the coupon at checkout, the sign-up through a referral link. Every check counts toward your monthly quota.
- The device ID saved with the account or the order, so your own data can answer questions beyond 30 days.
Get a free key 1,000 checks a month free, no card.