Provider
Use The Hosted Verification Service
Customer sites load the public widget from https://verify.be-online.ro,
receive a one-time hv-token, and verify that token on their backend before
accepting signup, login, checkout, password reset, or another protected action.
The site_key is public and goes in HTML. The site_secret
is private and must stay only on the backend.
Provider operator
1. Onboard The Customer Site
Run this once for each customer website or trust boundary:
powershell $env:HV_BASE_URL="https://verify.be-online.ro" $env:HV_ADMIN_TOKEN="<provider admin token>" npm run site:onboard -- create ` --origin https://app.customer.com ` --action signup.submit,login.submit,checkout.submit ` --verify
Send the customer these values:
text Provider URL: https://verify.be-online.ro Site key: hv_site_... Site secret: hv_secret_... Actions: signup.submit, login.submit, checkout.submit Origin: https://app.customer.com
Customer frontend
2. Add The Widget To The Form
Add the provider script once, then place the widget inside the protected form.
html
<script src="https://verify.be-online.ro/widget.js" async defer></script>
<form id="signup-form" method="post" action="/signup">
<label>
Email
<input name="email" type="email" autocomplete="email" required>
</label>
<label>
Password
<input name="password" type="password" autocomplete="new-password" required>
</label>
<div
class="hv-widget"
data-sitekey="hv_site_CUSTOMER_SITE_KEY"
data-action="signup.submit"
data-auto="true">
</div>
<button type="submit">Create account</button>
</form>
The widget writes a hidden form field named hv-token. If your backend needs
another field name, set data-response-field="human_check_token".
Optional Submit Guard
This keeps the user on the form until the widget has produced a token.
html
<script>
const form = document.querySelector("#signup-form");
const widget = form.querySelector(".hv-widget");
form.addEventListener("submit", async (event) => {
const token = form.elements["hv-token"]?.value;
if (token) return;
event.preventDefault();
try {
await widget.__humanVerificationWidget.execute();
if (form.elements["hv-token"]?.value) {
form.requestSubmit();
}
} catch {
// Keep the user on the form. The widget shows retry state.
}
});
</script>
Widget Events
js
const widget = document.querySelector(".hv-widget");
widget.addEventListener("hv:verified", (event) => {
console.log("Verified action:", event.detail.action);
});
widget.addEventListener("hv:error", (event) => {
console.warn("Verification error:", event.detail.error);
});
widget.addEventListener("hv:fallback", (event) => {
console.log("Fallback case:", event.detail.fallbackCaseId);
});
Customer backend
3. Verify Before Accepting The Action
Every protected backend route must verify the submitted token:
text POST https://verify.be-online.ro/v1/hv/siteverify Content-Type: application/x-www-form-urlencoded
| Field | Required | Value |
|---|---|---|
secret |
Yes | Customer backend site_secret. |
response |
Yes | The submitted hv-token. |
action |
Recommended | Expected action, such as signup.submit. |
hostname |
Optional | Expected hostname, such as app.customer.com. |
remoteip |
Optional | End-user IP for future risk tuning. |
Copy-paste backend examples
4. Code Examples
Node.js / Express
js
import express from "express";
const app = express();
const providerBase = process.env.HV_PROVIDER_BASE || "https://verify.be-online.ro";
const siteSecret = process.env.HV_SITE_SECRET;
app.use(express.urlencoded({ extended: false }));
async function verifyHumanToken({ token, action, hostname, remoteip }) {
const response = await fetch(`${providerBase}/v1/hv/siteverify`, {
method: "POST",
headers: { "content-type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
secret: siteSecret,
response: token || "",
action,
hostname,
remoteip,
}),
});
const result = await response.json();
if (!response.ok || !result.success) {
return {
ok: false,
errorCodes: result.error_codes || ["verification-failed"],
};
}
return { ok: true, clearanceTier: result.clearance_tier };
}
app.post("/signup", async (req, res) => {
const verification = await verifyHumanToken({
token: req.body["hv-token"],
action: "signup.submit",
hostname: "app.customer.com",
remoteip: req.ip,
});
if (!verification.ok) {
return res.status(403).json({
error: "human_verification_failed",
details: verification.errorCodes,
});
}
return res.status(201).json({ ok: true });
});
app.listen(3000);
Next.js App Router
js
const providerBase = process.env.HV_PROVIDER_BASE || "https://verify.be-online.ro";
export async function POST(request) {
const form = await request.formData();
const token = String(form.get("hv-token") || "");
const verifyResponse = await fetch(`${providerBase}/v1/hv/siteverify`, {
method: "POST",
headers: { "content-type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
secret: process.env.HV_SITE_SECRET,
response: token,
action: "signup.submit",
hostname: "app.customer.com",
}),
cache: "no-store",
});
const verification = await verifyResponse.json();
if (!verifyResponse.ok || !verification.success) {
return Response.json(
{
error: "human_verification_failed",
details: verification.error_codes || [],
},
{ status: 403 },
);
}
return Response.json({ ok: true });
}
PHP
php
<?php
$providerBase = getenv("HV_PROVIDER_BASE") ?: "https://verify.be-online.ro";
$siteSecret = getenv("HV_SITE_SECRET");
$token = $_POST["hv-token"] ?? "";
$fields = http_build_query([
"secret" => $siteSecret,
"response" => $token,
"action" => "signup.submit",
"hostname" => "app.customer.com",
]);
$ch = curl_init($providerBase . "/v1/hv/siteverify");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $fields,
CURLOPT_HTTPHEADER => ["Content-Type: application/x-www-form-urlencoded"],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
]);
$raw = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
$verification = json_decode($raw ?: "{}", true);
if ($httpCode < 200 || $httpCode >= 300 || empty($verification["success"])) {
http_response_code(403);
header("Content-Type: application/json");
echo json_encode([
"error" => "human_verification_failed",
"details" => $verification["error_codes"] ?? ["verification-failed"],
]);
exit;
}
header("Content-Type: application/json");
echo json_encode(["ok" => true]);
API contract
5. Success And Failure Responses
Success
json
{
"success": true,
"action": "signup.submit",
"hostname": "app.customer.com",
"clearance_tier": "low",
"challenge_ts": "2026-06-21T13:00:00.000Z",
"redeemed_at_ms": 1782046800000
}
Failure
json
{
"success": false,
"error_codes": ["timeout-or-duplicate"]
}
| Code | Meaning |
|---|---|
missing-input-secret |
Backend did not send the site secret. |
invalid-input-secret |
Secret is wrong or rotated. |
missing-input-response |
No hv-token was submitted. |
invalid-input-response |
Token is unknown, malformed, or expired. |
timeout-or-duplicate |
Token was already spent. Run the widget again. |
action-mismatch |
Backend action differs from widget action. |
hostname-mismatch |
Backend hostname check failed. |
Launch checks
6. Testing Checklist
- Load the page and confirm the widget appears.
- Submit without
hv-tokenand confirm the backend rejects it. - Submit with a valid token and confirm the backend accepts it.
- Submit the same token again and confirm replay is rejected.
- Use a wrong backend action and confirm
action-mismatch. - Test every production hostname, such as
https://app.customer.comandhttps://www.customer.com.
Provider-side canary check:
powershell $env:HV_PROVIDER_BASE="https://verify.be-online.ro" $env:HV_PROTECTED_SITE_URL="https://verify.be-online.ro/protected-example" npm run protected-site:audit