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.

Everything is a JSON 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:

base url
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.

header
X-API-Key: sf_live_xxxxxxxxxxxxxxxxxxxxxxxx
Keep your key secret. It carries your full quota — never ship it in front-end JavaScript or a mobile app. Call Fraud Filter from your server, or proxy through your own backend. If a key leaks, rotate it from the dashboard. A missing or invalid key returns 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.

PlanIncluded calls
Starter10 / month
GrowthUnlimited
Pay as you goPrepaid 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:

error
{ "detail": "Provide 'content' (or a title/description) to scan." }
StatusMeaning
400Bad request — missing/invalid input (e.g. no content, batch over 50 rows)
401Missing or invalid API key
403Key revoked, or not linked to the action
429Monthly call limit reached — upgrade
500Detection engine error — safe to retry

Scan content

POST/api/check-job

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

FieldTypeDescription
content *stringThe full text to analyze. Required unless you send title or image_data.
image_datastringBase64 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_mimestringImage type, e.g. image/png or image/jpeg. Defaults to image/png.
contact_emailstringSender address. Checked against your blocklist; a match forces a critical verdict.
titlestringJob/listing title (structured input).
companystringCompany or advertiser name.
salarystringStated pay or price.
descriptionstringAlias for content if you prefer.
locationstringStated location.

Example request

curl
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" }'
python
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
node.js
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

200 OK
{ "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

POST/api/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

FieldTypeDescription
items *array1–50 objects, each { "content": string, "contact_email"?: string }.
curl
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

200 OK
{ "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)

POST/api/batch/jobs

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

FieldTypeDescription
items *arrayUp to 10,000 rows, each { content, contact_email? }.
webhook_urlstringOptional — we POST a batch_job.completed event here when the job finishes.

Lifecycle

CallDoes
POST /api/batch/jobsCreate the job → { job_id, status:"pending", total }.
POST /api/batch/jobs/{job_id}/processScore 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

POST/api/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

FieldTypeDescription
domain *stringe.g. acme.com or jobs@acme.com (the host is extracted).
curl
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" }'
200 OK
{ "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

POST/api/check-brand-spoof

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

FieldTypeDescription
official_url *stringYour real page — the one being impersonated. Usually your login page.
suspect_urlstringThe suspected lookalike. Best option: it's what the domain forensics score.
suspect_textstring…or the suspect's HTML/visible text, if we can't fetch it.
suspect_image_datastring…or a base64 screenshot of it (raw or a data: URL).
suspect_image_mimestringe.g. image/png. Defaults to image/png.

Supply at least one of suspect_url, suspect_text or suspect_image_data.

curl
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" }'
200 OK
{ "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

POST/api/check-login-event

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

FieldTypeDescription
user_id *stringYour own end-user's id. Opaque to us — it's only the key we group history by.
device_fingerprintstringA stable per-device id from your client. Enables new-device detection.
ipstringSource IP of the event.
countrystringISO code or name. Drives new-country and impossible-travel detection.
actionstringWhat happened, e.g. login, password_change, payment_update, profile_edit. Money-moving / account-control actions raise the stakes.
user_agentstringClient user-agent (stored for the queue).
atstringISO timestamp of the event. Defaults to now.
curl
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" }'
200 OK
{ "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

POST/api/check-fraud-thread

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

FieldTypeDescription
events *arrayTwo or more communications. Each: channel (email/phone/sms/chat/letter), content, optional sender (address/number/name) and at (ISO timestamp).
claimed_identitystringWho the events collectively claim to be from, e.g. "Jane Okafor, CEO".
curl
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"} ] }'
200 OK
{ "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

POST/api/check-invoice

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

FieldTypeDescription
invoice_data *stringBase64 image or PDF-page of the new invoice (raw or data: URL).
invoice_mimestringe.g. image/png, application/pdf.
prior_datastringOptional prior invoice from the same vendor, to diff against.
prior_mimestringMIME of the prior invoice.
curl
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>" }'
200 OK
{ "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

POST/api/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

FieldTypeDescription
messages *arrayMessages in order. Each: direction ("them" or "me"), text, optional at (ISO timestamp).
contactstringWho the messages are from (name / handle / number).
curl
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?" } ] }'
200 OK
{ "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

POST/api/reverse-image

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

FieldTypeDescription
image_datastringBase64 image (raw or data: URL). Provide this or image_url.
image_mimestringe.g. image/png, image/jpeg.
image_urlstringA public URL we can fetch instead of base64.
curl
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" }'
200 OK
{ "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

POST/api/check-deepfake

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

FieldTypeDescription
image_datastringBase64 image (raw or data: URL). Provide this or image_url.
image_mimestringe.g. image/png, image/jpeg.
image_urlstringA public URL we can fetch instead of base64.
curl
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" }'
200 OK
{ "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

GET/api/key/status

Return the current plan and usage for the authenticating key. Doesn't consume quota.

curl
curl https://fraudfilter.space/api/key/status -H "X-API-Key: sf_live_your_key"
200 OK
{ "plan": "starter", "calls_used": 16, "calls_remaining": 984 }

Health

GET/api/health

Unauthenticated liveness check.

200 OK
{ "status": "ok", "version": "2.0.0", "stack": "Vercel + Supabase + Gemini" }

Scan response object

Returned by /api/check-job.

FieldTypeDescription
overall_risk_scoreinteger0–100, higher = riskier. Reflects the strongest threat present.
overall_verdictstringlow · medium · high · critical — see scale.
scam_categoriesobjectAll 7 categories, each { score, flags[], details }.
signalsobjecturgency, payment_request, contact_mismatch — each with detected + severity.
reasonsarrayRanked findings: { type, severity, description }.
recommendationsarrayActionable next-step strings.
explanationobject{ summary, confidence } — plain-English verdict + model confidence 0–100.
blacklistedobject?Present only on a blocklist hit: { value, reason }.
calls_remainingintegerCalls left in this billing period after this request.

Verdict scale

overall_verdict maps directly from overall_risk_score:

ScoreVerdictSuggested action
0–24lowLooks clean — allow.
25–49mediumSuspicious — warn or review.
50–79highLikely fraud — block or hold.
80–100criticalAlmost certainly fraud — block.

Scam categories

Every scan scores all seven, so a single call covers them at once:

KeyCovers
fake_jobFake/ghost job offers, unrealistic pay, chat-app-only hiring.
advance_feeUpfront fees, "pay to start", prize/inheritance scams.
phishingCredential/ID/bank-detail harvesting, spoofed links.
impersonationBrand/recruiter/agency impersonation, sender mismatch.
investmentGuaranteed returns, crypto/forex/USDT "opportunities".
romanceRapid emotional escalation, off-platform money asks.
fake_goodsCounterfeit/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

POST your endpoint
{ "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.

headers
X-FraudFilter-Event: scan.flagged X-FraudFilter-Signature: sha256=<hex digest>
python — verify
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):

node / express — verify
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); }
go — verify
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.

python — full check
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
Respond 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.