integration guide · private beta
Content Gate (reCAPTCHA alternative)
Updated May 12, 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 — stopping automated form spam without image puzzles, tracking cookies, or intrusive user prompts.
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.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.js and configure your form element:
<script
type="module"
src="https://siftfy.io/widget/v1.js"
integrity="sha384-ZBVXkcvAuiWioj6JPYij1U2LscKJTj8bDlgrB2+dWGxokmMK62k0eixPg39vQuPt"
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"
}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") {
return rejectSubmission();
}
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.
- Decisions: Returns
allow,review,block, orindeterminate. Handle transport errors andindeterminateas fail-safe queue or review conditions.