integration guide · private beta
Content Gate (reCAPTCHA alternative)
Updated October 11, 2026
Content Gate is a privacy-first alternative to reCAPTCHA and Turnstile. It binds a short-lived browser proof-of-work challenge to the exact text sent to Siftfy for spam classification, and scores every session from 0 to 100 for automation — stopping automated form spam without image puzzles, tracking cookies, or intrusive user prompts.
Try it first: the live demo is a contact form protected by widget v1.4. Send it a message and it shows the exact siteverify response a site's backend receives, bot object included. Point a headless browser at it and watch the decision change.
How it works
- Create a site: Register a site in Dashboard > Content Gate. New sites start in
monitormode. - Store secrets: Put the one-time
sf_secret_...in your backend environment variables or secret manager. Only the publicsf_site_...key goes into client-side code. - Embed widget: Add the immutable
widget/v1.4.jsscript tag and HTMLdata-siftfy-*form attributes. - Backend verification: Before running business logic, send the canonical form content, challenge, and proof to
POST /v1/content-gate/siteverify.
Frontend widget snippet
Load the immutable widget script from https://siftfy.io/widget/v1.4.js and configure your form element. Load it with the page, not on submit: its automation checks watch the form being filled in.
<script
type="module"
src="https://siftfy.io/widget/v1.4.js"
integrity="sha384-uxnDttDappw57cOUF2LyFLT/aoZBl9+RBIalHd43oqXkw3inAQIEfIdHQWL07Bzw"
crossorigin="anonymous"
></script>
<form
data-siftfy-site-key="sf_site_REPLACE_WITH_YOUR_SITE_KEY"
data-siftfy-action="contact_submit"
data-siftfy-fields="subject,message"
>
<input name="subject" type="text" required>
<textarea name="message" required></textarea>
<button type="submit">Send</button>
</form>Content Security Policy (CSP)
If your application enforces CSP headers, add these rules:
script-src 'self' https://siftfy.io;
connect-src 'self' https://api.siftfy.io;
worker-src 'self' blob:;Verification API contract
Send a POST request from your application backend to https://api.siftfy.io/v1/content-gate/siteverify.
Always include a customer-generated Idempotency-Key header to safely retry requests without double-evaluating challenges.
{
"challenge": "<siftfy_content_gate_challenge>",
"proof_nonce": "<siftfy_content_gate_proof>",
"expected_action": "contact_submit",
"expected_origin": "https://www.example.com",
"content": "Subject line\nMessage body"
}Act on decision, not on spam_probability
Every example below branches on decision. Do that. It already accounts for the score, the model's confidence ceiling, your site's thresholds and your mode, and it keeps accounting for them when the model changes.
Do not build an absolute threshold on spam_probability. The score is calibrated and then clamped to a ceiling that sits below your block threshold, so that the model can never reject a submission on its own word. A constant above that ceiling can never be reached: set 0.90 against a ceiling of 0.84 and the comparison never fires, so every action you credit to us was in fact carried by your own signals. Since 2026-09-21 siteverify returns that ceiling as max_confidence, so if you read the score at all, compare it against the ceiling in the same response rather than against a constant of your own — your logic then follows the ceiling up when a better checkpoint lands.
A score that has reached the ceiling is censored: it means “at least this”, not “this”. The clamp discards everything above it, so content the model reads as 0.86 and content it reads as 0.9999 both arrive at the same value, and above the clamp the score no longer ranks. It therefore does not support arithmetic — subtracting your own discount from it, or blending it with your own signals, operates on a lower bound and yields a confident-looking number with nothing underneath it. Whatever you subtract, every clamped message yields the same answer — ceiling minus credit — whether the text truly read 0.86 or 0.9999, so if that value falls just under your warning line it falls there for all of them, every time. Compare spam_probability against max_confidence to decide whether the model is saturated, then let your own evidence decide what to do; do not read the distance between the two as a quantity.
How often the ceiling fires depends on your traffic, and on some traffic it is the common case. With the clamp off for English, two production sites enforcing at the block band refused legitimate submissions — 16 of 38 ordinary transactional requests in one four-hour window, and 3 of 3 in another measured set — which re-scored at 0.9947–1.0000, where genuine spam also sits. That is why the clamp exists, and it means saturation is the clamp working as intended on text the model cannot separate: not a fault, but not a corner case either.
max_confidence is absent, never zero, when no ceiling is reported. Older inference builds report none, and an idempotent replay of a verification recorded before the field existed has none stored. Absent means unknown: fall back to decision rather than substituting 0, which makes every score look saturated, or 1, which makes none of them.
In enforce mode the decision that moves is review, not block.block requires the score to clear your block threshold and exceed the ceiling, so on content we have not seen before the model reaches review at most. block is reserved for content a human-confirmed spam report has already escalated. Treat review as “we think this is spam” and carry it with your own evidence; if you wire block as your only action trigger, it will rarely fire.
On paid sites with live escalation, TypeSafe decides the unsure band. Since 2026-09-23, when a paid site's score falls from its review_threshold up to and including max_confidence, we ask TypeSafe while the request waits (up to one second). A confident answer is the decision: block or allow with reason: "typesafe", in enforce and monitor mode alike. With no confident answer the decision is the review it would have been, with reason: "typesafe_unavailable", or "typesafe_budget" when the day's TypeSafe budget is spent; a review_as: "block" action still turns that review into block. On those sites a score strictly above the ceiling still means a human-confirmed spam report and blocks without TypeSafe, and a score below review_threshold is allowed without it. decision keeps its four values, so code that branches on it needs no change. Free sites are never escalated.
A bare email address is deliberately never sent to TypeSafe. When its score needs escalation, the result is review with reason: "insufficient_content", rather than a vendor error. For signups, combine solver velocity and deny lists with limits on new accounts and the resources they receive. A solved proof of work does not prove humanity or legitimate account use.
Who is submitting: signals, feedback, policy and the deny list
On a form whose only content is an identifier — an email, a phone number, a username — the owner typing their own address and a stranger typing it are byte-identical, so the model can only answer review. What separates them is who is submitting. Content Gate keys that on the submitter's IP address as the challenge endpoint sees it (the full IPv4 address, or the IPv6 /64), never on a fingerprint or a cookie.
{
"challenge_valid": true,
"decision": "block",
"reason": "velocity",
"verification_id": "sf_ver_3kQm0bq2S8m1Qx4v9Hn2dA",
"solver_id": "sf_solver_Z0v8r7P3mQ1s5yXw2cB9kA",
"signals": {
"solver_submissions_1h": 14,
"solver_distinct_content_24h": 63,
"site_action_submissions_1h": 40,
"solver_reports_other_sites_30d": 0
},
"error_codes": []
}verification_idis the handle for reporting this verification later, for 30 days. It is stable across idempotent retries.solver_idis the submitter as your site sees it. The same visitor has a differentsolver_idon every other site.reasonisscore,review_as,velocity,site_denylist,solver_reputation,cross_site_reputation,insufficient_content,typesafe,typesafe_unavailable,typesafe_budgetorbot.velocity,site_denylist,solver_reputation,cross_site_reputationandbotare decided before the model runs: nospam_probability, one usage unit. Escalation reasons are described under TypeSafe decisions.challenge_levelispoworinteractive, the level of the solved challenge (see step-up).signalscounts include the submission being verified:solver_submissions_1h,solver_distinct_content_24h(an estimate within about 1%),site_action_submissions_1h, andsolver_reports_other_sites_30d, the number of other site owners who reported this solver.
Step-up on one form: challenge_level
Widget v1.2 adds an opt-in interactive level for the form where a scripted sender costs you most, such as a sign-in link request. It is a speed bump for scripted senders, not a bot-proof check: the proof is harder (difficulty 20, about 0.9 s on a laptop), siteverify refuses the challenge with challenge_too_early until 1.5 s after it was issued, and the check must start from a user action. It shows no puzzle and works keyboard-only and with a screen reader.
<script
type="module"
src="https://siftfy.io/widget/v1.4.js"
integrity="sha384-uxnDttDappw57cOUF2LyFLT/aoZBl9+RBIalHd43oqXkw3inAQIEfIdHQWL07Bzw"
crossorigin="anonymous"
></script>
<form
data-siftfy-site-key="sf_site_REPLACE_WITH_YOUR_SITE_KEY"
data-siftfy-action="magic_link_request"
data-siftfy-fields="email"
data-siftfy-challenge-level="interactive"
>siteverify returns challenge_level (pow or interactive) from the signed challenge, so a client cannot upgrade it. Require it in your backend, and request it on every submission of that form. Asking for a step-up only when an address is unknown would reveal which addresses have accounts. From script, call window.SiftfyContentGateV12.execute(form, { challengeLevel: "interactive" }) inside the submit handler.
Bot resistance: the bot object and widget v1.4
Every challenge carries a bot score from 0 to 100, computed when the challenge is requested — the only request where SiftFy sees the visitor — from a versioned rule table (bot-rules-v2) over signals SiftFy collects itself: whether the request's headers match the browser its User-Agent claims (including the same Chrome version in the User-Agent and the browser's own client hints), whether the address is in a published cloud or hosting range (public data we refresh weekly), and, with widget v1.4, twelve yes/no automation checks such as navigator.webdriver, a headless build, a window larger than its own screen, or text that reached the page with no key pressed. Only the check results are sent, once, with the challenge request; nothing is stored on the device, nothing identifies the browser, and no third-party service is called. The score is sealed inside the challenge so the page cannot read it, and siteverify returns it:
"bot": {
"score": 94,
"reasons": ["automation", "client_mismatch"],
"rules_version": "bot-rules-v2"
}reasons is a subset of automation, client_mismatch, network and solver_reputation. bot is null for a challenge issued before the release or one where scoring failed; scoring fails open and never refuses a challenge.
Default block. On an enforce site a session scoring 90 or more is answered block with reason: "bot", for every widget version. A monitor site receives the score but its decision never changes. Set bot_block_threshold per action in Settings or through the policy API: omit it for the default, a whole number from 1 to 100, or null for off. Activity shows a "Blocked as bot" count per action. If the default ever misfires, SiftFy can switch it off platform-wide (CONTENT_GATE_BOT_DEFAULT_THRESHOLD=0) while scores keep being reported.
Verified agents (Pro). An agent can sign its request with Web Bot Auth (RFC 9421). When the backend forwards that signature as agent_signature, an action whose verified_agent_policy is allow or review exempts the verified agent from the bot block only: the spam classifier, deny list, reputation and velocity limits still decide. allow reports verified_agent_clean; review holds a clean submission for you. ignore is the default on every plan; the exemption needs Pro. The AgentDraft case study shows the production block-to-allow run and how to wire both sides.
<script
type="module"
src="https://siftfy.io/widget/v1.4.js"
integrity="sha384-uxnDttDappw57cOUF2LyFLT/aoZBl9+RBIalHd43oqXkw3inAQIEfIdHQWL07Bzw"
crossorigin="anonymous"
></script>v1.4 is a drop-in replacement for v1.3 and v1.2; read window.SiftfyContentGateV14 from script, and load it with the page rather than on submit so it sees the form being filled in. v1.3 stays available at its published hash. Content Gate is bot-resistant, not bot-proof: the score raises the cost of automation and does not prove anyone is human.
Upgrading to v1.4
| You load | Change | What you gain |
|---|---|---|
v1.js with data-siftfy-* attributes | Swap the src and integrity. The form attributes stay the same. | All twelve automation checks. Today only the request headers and network are scored. |
v1.1 calling execute() | Swap the tag and read window.SiftfyContentGateV14 instead of SiftfyContentGateV11. execute() resolves the same { challenge, proof }. | The same, plus challengeLevel on the result. |
v1.2 or v1.3 | Swap the tag and the global. data-siftfy-challenge-level works unchanged. | Window geometry, typing and screen checks, which catch a Playwright browser with its automation flags removed. |
Choosing a threshold
- Start in
monitor. Every response carriesbot.score; log it next to your own outcome for a week before you enforce. - Keep the default (90) for most forms. It needs two independent signals, or one that only automation produces, such as a window no real browser can have.
- Lower to 70 on a form where a scripted sender costs you most, such as a sign-in link request. A single strong signal, such as a User-Agent whose Chrome version disagrees with the browser's own client hints, then blocks on its own.
- Turn it off (
null) on an action that real automation must reach, such as a partner's integration test, and keep the content decision.
Every one of these fields is optional: signals is absent when our counter store is unavailable, and solver_id and signals are absent on challenges issued before 2026-09-22.
Report a verification that turned out to be abuse
When a bounce or complaint shows a submission was a stranger's address, report it with the same site secret you use for siteverify. reason is recipient_complaint, hard_bounce or manual. That solver is then answered block with reason: "solver_reputation" on that action for the action's block period (30 days by default). A second report of the same verification returns duplicate: true. A report never trains the text model — on an identifier form the content is the victim's own address.
POST https://api.siftfy.io/v1/feedback
Authorization: Bearer sf_secret_...
Content-Type: application/json
{"verification_id": "sf_ver_...", "label": "spam", "reason": "recipient_complaint"}Per-action policy
On the site's Settings tab, one card per action: review_as refuses the review band on that action only (on an identifier action that can be most of your real users, so check Activity first); max_solver_submissions_1h and max_solver_distinct_content_24h block a solver past either count; reputation_block_days sets how long one report blocks (1–90); and cross_site_min_owners blocks a solver reported by at least that many other owners (off by default, otherwise 2–20, so no single account can block anyone on your site); and the bot block threshold (bot_block_threshold, see bot resistance).
Keep the policy in code
The same fields can be read and replaced one action at a time with the site secret, so the policy can live in your repository and a scheduled job can put it back after a hand edit. For an action with no policy, GET returns the defaults with managed_by and managed_at null, and that body can be sent back with PUT unchanged. A field left out of a PUT takes its default, and PUT replaces only that action's policy.
The read-only managed_by and managed_at fields are accepted in a PUT body and ignored.
managed_at is the last sync in epoch seconds: every PUT sets it, even when nothing changed.
GET /v1/content-gate/sites/{site_id}/policies/{action}
Authorization: Bearer sf_secret_...
PUT /v1/content-gate/sites/{site_id}/policies/{action}
Authorization: Bearer sf_secret_...
Content-Type: application/json
{"review_as": "review", "reputation_block_days": 30,
"max_solver_submissions_1h": 5, "max_solver_distinct_content_24h": 3,
"cross_site_min_owners": 0, "bot_block_threshold": 70}
200 {"review_as": "review", "reputation_block_days": 30,
"max_solver_submissions_1h": 5, "max_solver_distinct_content_24h": 3,
"cross_site_min_owners": 0, "bot_block_threshold": 70,
"managed_by": "api", "managed_at": 1790121664}400invalid_action_policywith thefieldthat is out of range or unknown; nothing is stored.401invalid_secret: the secret is missing or wrong.403site_mismatch:{site_id}is not the secret's site.404action_not_found: the site does not allow that action.429rate_limited: over 60 requests a minute for the site, shared with the deny list and feedback; retry afterRetry-After.
The deny list
Authenticate with the site secret. PUT takes a solver_id or a verification_id, an optional action, and ttl_seconds from one hour to 90 days; a denied solver is answered block with reason: "site_denylist". A site holds up to 10,000 active entries. The same list is on the Settings tab.
PUT /v1/content-gate/sites/{site_id}/denylist
{"solver_id": "sf_solver_...", "action": "magic_link_request", "ttl_seconds": 2592000}
GET /v1/content-gate/sites/{site_id}/denylist?cursor=...
DELETE /v1/content-gate/sites/{site_id}/denylist?solver_id=sf_solver_...&action=magic_link_requestWhen the paid TypeSafe shadow trial is enabled, selected new Pro submissions are also sent, with credentials removed, to TypeSafe for background comparison. Since 2026-09-23, when live escalation is enabled, a paid submission in the unsure band is sent to TypeSafe on the request path and its confident answer becomes the decision, as described under TypeSafe decisions; a submission that is only an email address is never sent, and free sites never are. Read the provider-processing disclosure and the live escalation disclosure before enabling evaluation.
What happens to the text you send
Verification itself keeps no submission text. The terminal result we store is content-free, and the replay and idempotency hashes derived from your request are logically expired about ten minutes after the call.
Evaluation collection is separate, and it is the one case where the text does persist. If it is enabled for your site, the content you send to siteverify is stored for 30 days and then deleted, unless it is a single email address and nothing else, which is not stored at all. (Siftfy's own demo page and sign-in form are never collected.) It is stored as submitted — email addresses, links, phone numbers and IP addresses are not masked. Only credential material is removed before storage: private key blocks, Bearer tokens, and values labelled password, secret, api_key or token. Your customer and site identifiers are replaced with one-way pseudonyms. Records live in a private encrypted S3 bucket in the United States with access restricted to authorized operators and reviewers, and they are used to measure classification accuracy — not to train or fine-tune a model, and not sold. The one exception is a site that VectraSEO LLC, which operates Siftfy, runs itself: from 2026-09-27 its submissions that our reviewer has labeled may be used to train Siftfy's spam, AI-authorship and automation models and are kept for 12 months, including submissions labeled between 2026-09-22 and 2026-09-27. The training copy is made by a daily job, only while the submission's 30-day evaluation record still exists, as the Privacy Policy describes. From 2026-10-08, TypeSafe verdicts on Content Gate submissions from those sites may also be used to train Siftfy's spam model, kept for 12 months: only the spam or legitimate verdict is stored with the submission text, and verdicts given before 2026-10-08 are included while the 30-day evaluation record still exists. From 2026-10-13, /v1/predict text from accounts that VectraSEO LLC runs itself, already kept for the paid shadow trial or live escalation, may also be used to train Siftfy's spam model with TypeSafe's verdict or our reviewer's label, kept for 12 months from collection. This is forward-only: nothing collected before 2026-10-13 is copied. TypeSafe spam verdicts on that text are copied only up to three for each legitimate one, and another customer's /v1/predict text is never used. Only first-time authenticated verifications carry collectible text; browser challenges and retries are excluded.
The spam label corpus is a second, separate collection, and it is not collecting. It would exist only where you have authorized it in writing, no customer has, and it holds no records — every site running Content Gate today is operated by VectraSEO LLC, the company that operates Siftfy, so there is nobody able to give that authorization. It would take only the submissions your own honeypot caught and you reported to us as spam, never something our model merely scored as spam. For those, the submission text would be kept for 12 months and used to train and evaluate the model, and a derived record holding no submission text is kept with no fixed end date. That derived record's fields are enumerated exhaustively in the Privacy Policy; the two that matter to you when you build the integration are that we keep the name of the honeypot field and never its value, and that the submission reference you send us is stored as a one-way keyed pseudonym rather than as you sent it, so a reference that happens to be an email address does not become one in a record with no expiry.
This corpus is empty and cannot be backfilled. Everything already collected reached us while these documents said we did not train on it, so none of it is eligible. Both the 12-month window and that cut-off are constants in the service rather than settings, and a record that disagrees with either is refused rather than stored. If you have a honeypot backlog you want represented, that is a consent question and not a configuration one — write to us.
The 30-day window is a constant in the service, not a setting, so it cannot be configured up. This changed on September 19, 2026: before that date we masked identifiers, and records collected before then still hold masked text and keep their original window of up to 90 days.
You are the party who has to tell your visitors. They submit a form on your site, not ours, so your own privacy notice is where this belongs. If evaluation collection is enabled for your site, say that submitted form content is sent to Siftfy for spam classification and may be retained for up to 30 days for evaluation. If you have also authorized the spam label corpus — which no customer has done, and which is not collecting — say that content your honeypot flags as spam is retained for 12 months and used to train a spam-classification model, that it is kept as submitted rather than anonymized, and that a record without the content is kept for longer. Do not tell your visitors this data is anonymized or anonymous. It is not, we do not describe it that way, and a claim you make on our behalf is still a claim you made.
We can erase records at the level of a customer and a site, which is the only level at which we can find them — the text is not indexed by any visitor's identity, so we cannot locate a single submission on our own. A visitor asking for their submission to be deleted has to ask you; forward the request to hi@siftfy.io and we will erase your site's records.
The full schedule, the collection-failure reports, and what we do and do not keep for standalone /v1/predict calls are in the Privacy Policy.
Backend code examples
Node.js / JavaScript
const response = await fetch("https://api.siftfy.io/v1/content-gate/siteverify", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SIFTFY_CONTENT_GATE_SECRET}`,
"Content-Type": "application/json",
"Idempotency-Key": submissionId,
},
body: JSON.stringify(payload),
signal: AbortSignal.timeout(3000),
});
if (!response.ok) return queueForReview("verification_unavailable");
const result = await response.json();
if (!result.challenge_valid || result.decision === "indeterminate") {
return queueForReview("verification_incomplete");
}
if (result.mode === "enforce" && result.decision === "block") {
// reason "bot": the session scored at or above the action's bot block threshold.
// result.bot = { score, reasons, rules_version }; log it, never show it to the visitor.
return rejectSubmission(result.reason);
}
return result.decision === "review" ? queueForReview("content_policy") : accept();Python
import os
import requests
try:
response = requests.post(
"https://api.siftfy.io/v1/content-gate/siteverify",
headers={
"Authorization": f"Bearer {os.environ['SIFTFY_CONTENT_GATE_SECRET']}",
"Content-Type": "application/json",
"Idempotency-Key": submission_id,
},
json=payload,
timeout=3,
)
except requests.RequestException:
return queue_for_review("verification_unavailable")
if response.status_code != 200:
return queue_for_review("verification_unavailable")
result = response.json()
if not result.get("challenge_valid") or result.get("decision") == "indeterminate":
return queue_for_review("verification_incomplete")
if result.get("mode") == "enforce" and result.get("decision") == "block":
return reject_submission()Go
body, _ := json.Marshal(payload)
req, _ := http.NewRequestWithContext(ctx, http.MethodPost,
"https://api.siftfy.io/v1/content-gate/siteverify", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+os.Getenv("SIFTFY_CONTENT_GATE_SECRET"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", submissionID)
resp, err := (&http.Client{Timeout: 3 * time.Second}).Do(req)
if err != nil || resp.StatusCode != http.StatusOK {
return queueForReview("verification_unavailable")
}
defer resp.Body.Close()
var result struct {
ChallengeValid bool `json:"challenge_valid"`
Decision string `json:"decision"`
Mode string `json:"mode"`
}
if json.NewDecoder(resp.Body).Decode(&result) != nil ||
!result.ChallengeValid || result.Decision == "indeterminate" {
return queueForReview("verification_incomplete")
}
if result.Mode == "enforce" && result.Decision == "block" {
return rejectSubmission()
}
Modes & Content Decisions
- Monitor mode: Evaluates proofs and spam classifiers without asking your backend to enforce blocks. Ideal during initial integration.
- Enforce mode: Signals hard blocks when content is confirmed spam, and blocks sessions whose bot score reaches the action's bot block threshold (90 by default).
- Decisions: Returns
allow,review,block, orindeterminate. Handle transport errors andindeterminateas fail-safe queue or review conditions.
Trust badge
Optional. Place a small "Secured by Siftfy" badge near a protected form or in your footer so visitors can see the page is guarded and verify it by following the link back to this page — the same transparency pattern reCAPTCHA and Cloudflare Turnstile use for their badges. The badge is a static, self-contained snippet: no script tag, no extra network request, and no tracking.
<a
href="https://siftfy.io/docs/content-gate"
target="_blank"
rel="noopener"
aria-label="Secured by Siftfy Content Gate — verified. Opens siftfy.io in a new tab."
style="display:inline-flex;align-items:center;gap:6px;padding:6px 12px 6px 9px;border-radius:999px;border:1px solid #e8e3da;background:#ffffff;text-decoration:none;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;font-size:12.5px;line-height:1;color:#5a5a66;"
>
<svg width="16" height="16" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<circle cx="12" cy="12" r="11" fill="#3D3DCE" />
<path d="M7.2 12.6 L10.4 15.8 L16.8 8.6" fill="none" stroke="#FFFFFF" stroke-width="2.3" stroke-linecap="round" stroke-linejoin="round" />
</svg>
<span>Secured by <strong style="color:#1a1a1f;font-weight:600;">Siftfy</strong></span>
</a><a
href="https://siftfy.io/docs/content-gate"
target="_blank"
rel="noopener"
aria-label="Secured by Siftfy Content Gate — verified. Opens siftfy.io in a new tab."
style="display:inline-flex;align-items:center;gap:6px;padding:6px 12px 6px 9px;border-radius:999px;border:1px solid rgba(255,255,255,0.14);background:#1a1a1f;text-decoration:none;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;font-size:12.5px;line-height:1;color:#b7b7c2;"
>
<svg width="16" height="16" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<circle cx="12" cy="12" r="11" fill="#6F6FE8" />
<path d="M7.2 12.6 L10.4 15.8 L16.8 8.6" fill="none" stroke="#FFFFFF" stroke-width="2.3" stroke-linecap="round" stroke-linejoin="round" />
</svg>
<span>Secured by <strong style="color:#f3eee4;font-weight:600;">Siftfy</strong></span>
</a>Pick whichever variant matches the surface you're placing it on, or swap the two inline colors yourself to match your own background. The badge links to this page so visitors — and search crawlers — can confirm what "Secured by Siftfy" means.