Customer integration

Implementation Docs

Console
verify.be-online.ro

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

  1. Load the page and confirm the widget appears.
  2. Submit without hv-token and confirm the backend rejects it.
  3. Submit with a valid token and confirm the backend accepts it.
  4. Submit the same token again and confirm replay is rejected.
  5. Use a wrong backend action and confirm action-mismatch.
  6. Test every production hostname, such as https://app.customer.com and https://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