API Reference
One HTTP call scores any message, email, marketplace listing or job post across seven scam types — and returns a risk score, a verdict, a per-category breakdown, and plain-English recommendations. Usually under a second.
POST with your API key in a header. No SDK required — curl works. Get a key from the dashboard.Base URL
All endpoints are served under your Fraud Filter origin:
https://fraudfilter.space/api
So the scan endpoint is https://fraudfilter.space/api/check-job. All requests must be HTTPS.
Prefer to click around first? Try every request live with your own key in the API playground, or download the OpenAPI 3.1 spec to generate a client.
Official SDKs: npm i @fraudfilter/sdk (JS/TS) · pip install fraudfilter (Python). Both wrap the endpoints below and support text + screenshot scanning.
Authentication
Authenticate every request with your secret API key in the X-API-Key header. Keys look like sf_live_… and are created, rotated and revoked from the dashboard.
X-API-Key: sf_live_xxxxxxxxxxxxxxxxxxxxxxxx
401; a revoked key returns 403.
Quota & rate limits
Each successful scan consumes one call from your plan's monthly allowance. Responses include calls_remaining so you can track usage inline.
| Plan | Included calls |
|---|---|
| Starter | 10 / month |
| Growth | Unlimited |
| Pay as you go | Prepaid credits — 1 per scan, no monthly reset |
When the allowance is exhausted, scan endpoints return 429. Batch requests scan as many rows as remaining quota allows and mark the rest skipped_quota rather than failing the whole request. Check the live counter any time with GET /api/key/status.
Pay-as-you-go keys draw down a prepaid credit balance instead of a monthly allowance — each scan uses one credit, calls_remaining is your remaining credits, and at zero the API returns 429 until you top up. Buy or top up credit packs from the billing screen.
Errors
Errors use standard HTTP status codes and a JSON body with a human-readable detail:
{ "detail": "Provide 'content' (or a title/description) to scan." }
| Status | Meaning |
|---|---|
400 | Bad request — missing/invalid input (e.g. no content, batch over 50 rows) |
401 | Missing or invalid API key |
403 | Key revoked, or not linked to the action |
429 | Monthly call limit reached — upgrade |
500 | Detection engine error — safe to retry |
Scan content
The core endpoint. Send any text — an email, DM, job post, marketplace listing, investment pitch — and get a full fraud analysis. Provide content for free-form text, or use the structured job fields; at least one of content or title is required.
Business workspace keys also get org-scoped BEC guards layered on top of the content score: a message referencing a registered vendor from a wrong domain/bank account adds vendor_deviation to the response, and one impersonating a registered executive from a non-registered address adds executive_impersonation — either escalates the verdict to at least high, however clean the text looks. Manage these in the Vendors and Executives console screens.
Every scan also runs a deterministic West-African / Nigeria fraud layer. When locally-specific markers appear — BVN/NIN/OTP credential harvesting, EFCC/NDLEA/bank authority-impersonation demanding payment, 419 advance-fee language, or fake WAEC/JAMB/NPower portal fees — they're returned as regional_signals (each with a signal, category, severity and the matched terms) and the strongest ones escalate the verdict. Simply naming a Nigerian bank or agency is not enough on its own — escalation needs a money, credential, or ID request alongside it.
Body parameters
| Field | Type | Description |
|---|---|---|
content * | string | The full text to analyze. Required unless you send title or image_data. |
image_data | string | Base64 of a screenshot (raw or a data:image/png;base64,… URL). The model reads the text in the image (built-in OCR) and scores it. Max ~5MB. Can be sent with or without content. |
image_mime | string | Image type, e.g. image/png or image/jpeg. Defaults to image/png. |
contact_email | string | Sender address. Checked against your blocklist; a match forces a critical verdict. |
title | string | Job/listing title (structured input). |
company | string | Company or advertiser name. |
salary | string | Stated pay or price. |
description | string | Alias for content if you prefer. |
location | string | Stated location. |
Example request
curl -X POST https://fraudfilter.space/api/check-job \
-H "X-API-Key: sf_live_your_key" \
-H "Content-Type: application/json" \
-d '{
"content": "Pay a $150 activation fee to start. Earn $25k/week from home!",
"contact_email": "globalopps2024@gmail.com"
}'
import requests
r = requests.post(
"https://fraudfilter.space/api/check-job",
headers={"X-API-Key": "sf_live_your_key"},
json={"content": message_text, "contact_email": sender},
)
data = r.json()
if data["overall_risk_score"] >= 50:
block(message_text) # high / critical
const res = await fetch("https://fraudfilter.space/api/check-job", {
method: "POST",
headers: { "X-API-Key": process.env.FRAUDFILTER_KEY, "Content-Type": "application/json" },
body: JSON.stringify({ content: messageText, contact_email: sender }),
});
const data = await res.json();
Example response critical
{
"overall_risk_score": 96,
"overall_verdict": "critical",
"scam_categories": {
"advance_fee": { "score": 95, "flags": ["upfront_fee"], "details": "Asks for a $150 fee before any work." },
"fake_job": { "score": 88, "flags": ["unrealistic_pay"], "details": "$25k/week is not credible." }
// …phishing, impersonation, investment, romance, fake_goods (all 7 always present)
},
"signals": {
"urgency": { "detected": true, "phrases": ["start now"], "severity": "high" },
"payment_request": { "detected": true, "methods": ["fee"], "amount_requested": "$150", "severity": "critical" },
"contact_mismatch": { "detected": true, "severity": "medium" }
},
"reasons": [ { "type": "advance_fee", "severity": "critical", "description": "Upfront payment required." } ],
"recommendations": [ "Do not pay any fee — legitimate jobs never charge to start." ],
"explanation": { "summary": "Classic advance-fee job scam…", "confidence": 97 },
"blacklisted": { "value": "globalopps2024@gmail.com", "reason": "reported recruiter" },
"calls_remaining": 987
}
See the full field reference below. blacklisted is only present when the sender matches your blocklist.
Batch scan
Score up to 50 rows in one request — ideal for triaging a CSV export of listings or inbound messages. Rows are scanned concurrently. Only successfully scored rows consume quota.
Body parameters
| Field | Type | Description |
|---|---|---|
items * | array | 1–50 objects, each { "content": string, "contact_email"?: string }. |
curl -X POST https://fraudfilter.space/api/batch-scan \
-H "X-API-Key: sf_live_your_key" -H "Content-Type: application/json" \
-d '{ "items": [
{ "content": "Remote data entry, $40/hr, no experience…", "contact_email": "hr@acme.co" },
{ "content": "Send 0.2 BTC to double your money", "contact_email": "x@telegram" }
] }'
Response
{
"results": [
{ "index": 0, "status": "ok", "verdict": "medium", "risk_score": 38,
"top_category": "fake_job", "summary": "Pay looks too high for the role." },
{ "index": 1, "status": "ok", "verdict": "critical", "risk_score": 94,
"top_category": "investment", "summary": "Guaranteed-return crypto scam." }
],
"scanned": 2,
"total": 2,
"calls_remaining": 985
}
Each result status is one of: ok, skipped (empty row), skipped_quota (beyond your remaining allowance), or error (with an error string). Results preserve their input index.
Batch jobs (async, up to 10k)
The synchronous batch caps at 50 rows. For screening tens of thousands of listings, submit them as a job: you get a job_id back immediately, then drain the work in chunks and poll for results. There's no queue to manage — each /process call scores a slice, so it fits inside serverless limits and you (or a cron) simply call it until the job is done.
Body parameters
| Field | Type | Description |
|---|---|---|
items * | array | Up to 10,000 rows, each { content, contact_email? }. |
webhook_url | string | Optional — we POST a batch_job.completed event here when the job finishes. |
Lifecycle
| Call | Does |
|---|---|
POST /api/batch/jobs | Create the job → { job_id, status:"pending", total }. |
POST /api/batch/jobs/{job_id}/process | Score the next chunk (~15 rows). Repeat until done:true. Each scored row uses one credit; if you run out, the rest are marked skipped_quota. |
GET /api/batch/jobs/{job_id} | Read-only status + per-status counts. |
GET /api/batch/jobs/{job_id}/results?offset=&limit= | Per-row results in original order (idx, status, risk_score, verdict, top_category, summary). |
The SDKs wrap this in one call — runBatchJob(items) (JS) / run_batch_job(items) (Python) create the job, drive it to completion, and return every result.
Verify domain
Check whether a domain can actually send legitimate email — MX, SPF, DMARC and DKIM — over DNS. A company that can't receive mail or has no sender authentication is a strong impersonation/spoofing signal. Accepts a bare domain or a full email address.
Body parameters
| Field | Type | Description |
|---|---|---|
domain * | string | e.g. acme.com or jobs@acme.com (the host is extracted). |
curl -X POST https://fraudfilter.space/api/verify-domain \
-H "X-API-Key: sf_live_your_key" -H "Content-Type: application/json" \
-d '{ "domain": "acme.com" }'
{
"domain": "acme.com",
"resolves": true,
"risk_score": 0,
"verdict": "low",
"is_free_email": false,
"checks": {
"mx": { "pass": true, "detail": "2 mail server(s)" },
"spf": { "pass": true, "detail": "v=spf1 include:_spf.google.com ~all" },
"dmarc": { "pass": true, "detail": "policy: reject" },
"dkim": { "pass": true, "detail": "selector 'google' found" }
},
"mx_records": ["10 aspmx.l.google.com."],
"flags": [],
"summary": "Mail infrastructure and sender authentication look legitimate.",
"calls_remaining": 984
}
A non-existent domain returns resolves: false with risk_score: 95. Free providers (gmail.com, outlook.com, …) return is_free_email: true — judge the individual sender, not the domain.
Brand spoof detection
Outbound brand protection: give us your real page and a suspected lookalike, and we tell you whether the suspect is a clone built to harvest your users' credentials.
Two layers run independently. Domain forensics are deterministic — typosquats, homoglyphs (paypa1, pay-pal), your brand buried in someone else's subdomain, punycode, bolted-on words like secure or verify. Content comparison fetches both pages and scores how closely the suspect reproduces your branding and sign-in UI. The domain layer needs no page fetch, so a phishing page that's already been taken down — or that cloaks automated requests — still gets a verdict.
Body parameters
| Field | Type | Description |
|---|---|---|
official_url * | string | Your real page — the one being impersonated. Usually your login page. |
suspect_url | string | The suspected lookalike. Best option: it's what the domain forensics score. |
suspect_text | string | …or the suspect's HTML/visible text, if we can't fetch it. |
suspect_image_data | string | …or a base64 screenshot of it (raw or a data: URL). |
suspect_image_mime | string | e.g. image/png. Defaults to image/png. |
Supply at least one of suspect_url, suspect_text or suspect_image_data.
curl -X POST https://fraudfilter.space/api/check-brand-spoof \
-H "X-API-Key: sf_live_your_key" -H "Content-Type: application/json" \
-d '{
"official_url": "https://acme.com/login",
"suspect_url": "https://acme-secure-login.top/signin"
}'
{
"risk_score": 90,
"verdict": "critical",
"same_domain": false,
"lookalike_domain": true,
"domain_score": 85,
"domain_signals": [
{ "signal": "brand_plus_extras", "severity": "critical",
"detail": "'acme-secure-login' is the brand name with 'securelogin' bolted on…" }
],
"clone_likelihood": 93,
"is_credential_harvester": true,
"impersonated_brand": "Acme",
"content_similarity": 96,
"suspect": {
"domain": "acme-secure-login.top",
"source": "fetched",
"has_password_field": true,
"posts_to": ["collector-9x.top"]
},
"flags": ["Its login form submits to a different domain (collector-9x.top) — credentials leave the site."],
"recommendations": ["File a takedown with the registrar."],
"explanation": { "summary": "Near-identical clone of your sign-in page.", "confidence": "high" },
"calls_remaining": 983
}
content_similarity is how much of your page's copy the suspect reproduces (0–100). clone_likelihood is null when we couldn't reach the suspect page and scored its domain alone — in that case suspect.source is "unreachable". If the suspect sits on your own registrable domain, same_domain is true and the risk is 0: it's your page, not a forgery.
Account-takeover check
Stream your own users' login and security events and we score each one for account takeover. Unlike the content checks, this is a behavioural check: every event is compared against that user's own history, so a single sign-in that's fine in isolation is flagged when it's a new device, from a new country, doing something sensitive. No model runs — it's deterministic signal math, so it's fast and cheap. Requires a Business workspace key (events are scoped to your organization).
Body parameters
| Field | Type | Description |
|---|---|---|
user_id * | string | Your own end-user's id. Opaque to us — it's only the key we group history by. |
device_fingerprint | string | A stable per-device id from your client. Enables new-device detection. |
ip | string | Source IP of the event. |
country | string | ISO code or name. Drives new-country and impossible-travel detection. |
action | string | What happened, e.g. login, password_change, payment_update, profile_edit. Money-moving / account-control actions raise the stakes. |
user_agent | string | Client user-agent (stored for the queue). |
at | string | ISO timestamp of the event. Defaults to now. |
curl -X POST https://fraudfilter.space/api/check-login-event \
-H "X-API-Key: sf_live_your_key" -H "Content-Type: application/json" \
-d '{
"user_id": "user_8f21",
"device_fingerprint": "d3v-9a1c",
"ip": "102.89.x.x",
"country": "NG",
"action": "payment_update"
}'
{
"risk_score": 90,
"verdict": "critical",
"user_id": "user_8f21",
"is_new_device": true,
"is_new_ip": true,
"is_new_country": true,
"signals": [
{ "code": "sensitive_action_new_context", "severity": "critical",
"detail": "'payment_update' from a new device/location…" }
],
"recommendations": ["Challenge this session with step-up authentication (MFA / re-verify)."],
"baseline_events": 14,
"calls_remaining": 982
}
The first event you ever send for a user_id is a quiet baseline (verdict low) — there's nothing to compare it against yet. As history accumulates the score sharpens. Signal codes include new_device, new_country, impossible_travel, sensitive_action_new_context, dormant_then_sensitive and ip_velocity. Risky events (score ≥ 25) also fire your registered webhooks and appear in the Takeover queue of your Business console.
Multi-channel correlation
Modern BEC blends channels: an email, then a phone call "to confirm", then an SMS with the account number. Each looks weak alone — the attack is the coordination. Send two or more communications about the same matter (on any mix of channels) and we score them as one attempt. A deterministic pass catches the hard cross-channel tells (the same amount or payout account pushed on multiple channels, urgency everywhere, a tight timeframe); a model pass judges the semantic coordination on top — so you still get a verdict even if the model is momentarily unavailable.
Body parameters
| Field | Type | Description |
|---|---|---|
events * | array | Two or more communications. Each: channel (email/phone/sms/chat/letter), content, optional sender (address/number/name) and at (ISO timestamp). |
claimed_identity | string | Who the events collectively claim to be from, e.g. "Jane Okafor, CEO". |
curl -X POST https://fraudfilter.space/api/check-fraud-thread \
-H "X-API-Key: sf_live_your_key" -H "Content-Type: application/json" \
-d '{
"claimed_identity": "Jane Okafor, CEO",
"events": [
{"channel": "email", "content": "Please wire $48,000 to the new account.", "at": "2026-07-10T09:00:00"},
{"channel": "phone", "content": "Calling to confirm the $48k transfer I emailed - today.", "at": "2026-07-10T09:20:00"}
]
}'
{
"risk_score": 85,
"verdict": "critical",
"coordinated": true,
"channels": ["EMAIL", "PHONE CALL"],
"coordination_signals": [
{ "code": "same_amount_across_channels", "severity": "high",
"detail": "The same amount (48000) is pushed on 2 separate channels." }
],
"scheme": "ceo_fraud",
"the_ask": "Wire $48,000 to a new account today.",
"recommendations": ["Verify the request with the executive on a known, trusted number before paying."],
"calls_remaining": 981
}
Signal codes include multi_channel, same_amount_across_channels, same_account_across_channels, urgency_across_channels and tight_timeframe. The deterministic verdict only reaches high when a repeated ask (same amount/account) is corroborated by cross-channel time pressure — a single co-occurrence stays medium and defers to the model. model carries the full semantic analysis (or null if the model was unavailable and the verdict came from the deterministic signals alone).
Invoice / BEC screening
Extract an invoice's payment fields from an image or PDF page and — optionally — diff them against a prior invoice from the same vendor. A changed bank account or remittance email between two invoices is the classic invoice-redirect (BEC/VEC) tell. Business keys also get a cross-check against your registered vendor baseline.
Body parameters
| Field | Type | Description |
|---|---|---|
invoice_data * | string | Base64 image or PDF-page of the new invoice (raw or data: URL). |
invoice_mime | string | e.g. image/png, application/pdf. |
prior_data | string | Optional prior invoice from the same vendor, to diff against. |
prior_mime | string | MIME of the prior invoice. |
curl -X POST https://fraudfilter.space/api/check-invoice \
-H "X-API-Key: sf_live_your_key" -H "Content-Type: application/json" \
-d '{ "invoice_data": "<base64-image-or-pdf>", "prior_data": "<base64-prior-invoice>" }'
{
"risk_score": 90,
"verdict": "high",
"flags": ["bank account changed since the last invoice (NL12… → NL99…)."],
"fields": { "vendor": "Acme Supplies", "iban": "NL99…", "amount": "12,400.00" },
"prior_fields": { "iban": "NL12…" },
"changes": [{ "field": "iban", "was": "NL12…", "now": "NL99…" }],
"vendor_flags": [],
"calls_remaining": 981
}
A changed payment field or a mismatch against a registered vendor floors the verdict at high regardless of how clean the document looks.
Conversation thread scan
Score a whole conversation, not each message in isolation. Romance and grooming scams reveal themselves in the arc — love-bombing → building trust → isolation → the financial ask. Send the messages in order (theirs and yours) and get the trajectory read.
Body parameters
| Field | Type | Description |
|---|---|---|
messages * | array | Messages in order. Each: direction ("them" or "me"), text, optional at (ISO timestamp). |
contact | string | Who the messages are from (name / handle / number). |
curl -X POST https://fraudfilter.space/api/thread-scan \
-H "X-API-Key: sf_live_your_key" -H "Content-Type: application/json" \
-d '{ "contact": "+1 555…", "messages": [
{ "direction": "them", "text": "My love, I feel so close to you already…" },
{ "direction": "them", "text": "My account is frozen — could you send $500 in gift cards?" }
] }'
{
"overall_risk_score": 88,
"overall_verdict": "high",
"scam_type": "romance",
"stage": "financial_ask",
"patterns": ["love_bombing", "gift_card_request"],
"financial_asks": ["$500 in gift cards"],
"recommendations": ["Do not send money or gift cards."],
"calls_remaining": 980
}
Reverse-image lookup
Detect a stolen or stock/model photo — an image that already appears across the web. A strong catfish / fake-listing tell. Provide the image as base64 or a URL we can fetch. Requires the reverse-image add-on to be enabled on your account (otherwise returns 503).
Body parameters
| Field | Type | Description |
|---|---|---|
image_data | string | Base64 image (raw or data: URL). Provide this or image_url. |
image_mime | string | e.g. image/png, image/jpeg. |
image_url | string | A public URL we can fetch instead of base64. |
curl -X POST https://fraudfilter.space/api/reverse-image \
-H "X-API-Key: sf_live_your_key" -H "Content-Type: application/json" \
-d '{ "image_url": "https://example.com/profile.jpg" }'
{
"status": "ok",
"matched": true,
"signal": {
"signal": "image_reused_across_web",
"severity": "high",
"reason": "This exact image already appears on 6 different websites…"
},
"best_guess": "stock photo businessman",
"calls_remaining": 979
}
signal is null when nothing was found. It's a supporting signal — surfaced on /api/check-job too when you attach a screenshot.
Deepfake / AI-image check
Detect a deepfake / face-swap or an AI-generated image — a fake-profile tell that catches synthetic photos appearing nowhere else online (complements reverse-image). Provide the image as base64 or a URL. Requires the deepfake add-on to be enabled (otherwise returns 503).
Body parameters
| Field | Type | Description |
|---|---|---|
image_data | string | Base64 image (raw or data: URL). Provide this or image_url. |
image_mime | string | e.g. image/png, image/jpeg. |
image_url | string | A public URL we can fetch instead of base64. |
curl -X POST https://fraudfilter.space/api/check-deepfake \
-H "X-API-Key: sf_live_your_key" -H "Content-Type: application/json" \
-d '{ "image_url": "https://example.com/profile.jpg" }'
{
"status": "ok",
"matched": true,
"signal": {
"signal": "ai_generated_image",
"severity": "high",
"reason": "This image looks AI-generated, not a real photo…"
},
"scores": { "deepfake": 0.08, "ai_generated": 0.94 },
"calls_remaining": 978
}
Key status
Return the current plan and usage for the authenticating key. Doesn't consume quota.
curl https://fraudfilter.space/api/key/status -H "X-API-Key: sf_live_your_key"
{ "plan": "starter", "calls_used": 16, "calls_remaining": 984 }
Health
Unauthenticated liveness check.
{ "status": "ok", "version": "2.0.0", "stack": "Vercel + Supabase + Gemini" }
Scan response object
Returned by /api/check-job.
| Field | Type | Description |
|---|---|---|
overall_risk_score | integer | 0–100, higher = riskier. Reflects the strongest threat present. |
overall_verdict | string | low · medium · high · critical — see scale. |
scam_categories | object | All 7 categories, each { score, flags[], details }. |
signals | object | urgency, payment_request, contact_mismatch — each with detected + severity. |
reasons | array | Ranked findings: { type, severity, description }. |
recommendations | array | Actionable next-step strings. |
explanation | object | { summary, confidence } — plain-English verdict + model confidence 0–100. |
blacklisted | object? | Present only on a blocklist hit: { value, reason }. |
calls_remaining | integer | Calls left in this billing period after this request. |
Verdict scale
overall_verdict maps directly from overall_risk_score:
| Score | Verdict | Suggested action |
|---|---|---|
| 0–24 | low | Looks clean — allow. |
| 25–49 | medium | Suspicious — warn or review. |
| 50–79 | high | Likely fraud — block or hold. |
| 80–100 | critical | Almost certainly fraud — block. |
Scam categories
Every scan scores all seven, so a single call covers them at once:
| Key | Covers |
|---|---|
fake_job | Fake/ghost job offers, unrealistic pay, chat-app-only hiring. |
advance_fee | Upfront fees, "pay to start", prize/inheritance scams. |
phishing | Credential/ID/bank-detail harvesting, spoofed links. |
impersonation | Brand/recruiter/agency impersonation, sender mismatch. |
investment | Guaranteed returns, crypto/forex/USDT "opportunities". |
romance | Rapid emotional escalation, off-platform money asks. |
fake_goods | Counterfeit/non-existent listings, too-good-to-be-true deals. |
Webhooks
Register endpoints in the dashboard to receive a signed POST whenever a scan's risk score meets your configured threshold. Fires from both /api/check-job and /api/batch-scan.
Payload
{
"event": "scan.flagged",
"created_at": "2026-06-21T09:14:00Z",
"data": {
"risk_score": 94,
"verdict": "critical",
"sender": "x@telegram",
"content_preview": "Send 0.2 BTC to double your money…",
"blacklisted": null
}
}
Verifying the signature
Each delivery carries two headers. Verify X-FraudFilter-Signature with an HMAC-SHA256 of the raw request body using your webhook's signing secret, then compare in constant time.
X-FraudFilter-Event: scan.flagged
X-FraudFilter-Signature: sha256=<hex digest>
import hmac, hashlib
def verify(raw_body: bytes, header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header)
Node.js — verify over the raw body (register the route with a raw body parser so the bytes aren't re-serialized):
const crypto = require("crypto");
// app.post("/webhooks/fraudfilter", express.raw({ type: "application/json" }), handler)
function verify(rawBody, header, secret) {
const expected = "sha256=" +
crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
const a = Buffer.from(expected), b = Buffer.from(header || "");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
)
func verify(rawBody []byte, header, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(rawBody)
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(header))
}
Preventing replays
The signed body carries a UTC created_at. Because it's inside the signature, an attacker can't alter it or forge a fresh one — so after the signature checks out, reject any delivery whose created_at is older than a short tolerance (e.g. 5 minutes). Combined with an idempotency key on created_at + sender, this stops a captured request from being replayed later.
import json
from datetime import datetime, timezone
def accept(raw_body: bytes, sig: str, secret: str, tolerance=300) -> bool:
if not verify(raw_body, sig, secret):
return False
ts = datetime.fromisoformat(json.loads(raw_body)["created_at"].replace("Z", "+00:00"))
age = (datetime.now(timezone.utc) - ts).total_seconds()
return 0 <= age <= tolerance # reject stale / future-dated deliveries
2xx within a few seconds to acknowledge. Deliveries are best-effort; build your handler to be idempotent.Need something not covered here? Email support@fraudfilter.space or open the dashboard.