Documentation
Quick start
Create an account, add a site and list its origins. Use the site key on the page and keep the secret on your backend.
1. Get a token in the browser
<script src="https://nolgard.com/v1/client.js" data-site-key="SITE_KEY" defer></script>
<script>
document.querySelector('#signup').addEventListener('submit', async (event) => {
event.preventDefault();
await Nolgard.ready;
const token = await Nolgard.execute({ action: 'signup' });
const form = event.target;
const response = await fetch('/signup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token, email: form.elements.email.value })
});
// Show your own success or error message here.
});
</script>execute() returns an opaque token. It carries no score; the decision comes back from the verify call on your server. Get a new token on every submit. Tokens are single-use and expire after 5 minutes.
2. Verify it on your backend
const response = await fetch('https://nolgard.com/v1/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
secret: process.env.NOLGARD_SECRET,
token,
remoteIp, // the visitor's IP as your server sees it
action: 'signup',
account: { email } // optional, see Account signals
})
});
const result = await response.json();
if (!result.success) {
if (result.error === 'invalid-secret') return verificationUnavailable(); // config: fail closed
if (result.error === 'over-quota') return createAccount(); // fail open
return reject(); // bad token
}
switch (result.decision) {
case 'allow':
case 'limit': return createAccountWithTrial(result.grant.credits ?? result.grant.share);
case 'challenge': return askForYourOwnStepUp();
case 'shadow': return pretendSuccess(); // no account, no email, no credits
case 'block': return reject();
default: return reject();
}Keep the secret on the server. For a runnable example, see the Node.js guide.
Analytics gating
Nolgard.gate(() => {
loadAnalytics(); // Plausible, PostHog, Google Analytics, ...
});If client hints are enabled for the site, the callback runs only for visitors judged human. Otherwise, it runs after any successful check. Use this for analytics filtering, never access control.
Integrations
More
Other platforms
Account signals
Send details of the account your backend is about to create or log in to:
{ "secret": "SITE_SECRET", "token": "...", "action": "signup",
"account": { "id": "user_123", "email": "jane+trial@example.com" } }Both fields are optional. Use a stable ID for each real account. Sending these fields can add the following to the response:
repeat.deviceAccounts: distinct accounts seen from this browser recently.repeat.networkAccounts: distinct accounts seen from this network recently.repeat.emailAliases: different spellings of the same mailbox, such as plus addresses.email.domainandemail.disposable: the domain and whether it provides throwaway addresses.
Shadow and block modes give repeat accounts limit. Use the counts for your own rules, such as one free trial per mailbox. Set the repeat threshold and cost per abused event in site settings.
Account IDs and emails are stored as salted hashes only. Raw emails are not stored.
Verify API
POST https://nolgard.com/v1/verify with a JSON body. Call it from your backend only.
CI test keys and the POST /v1/feedback API: Test keys and feedback.
Request
| Field | Type | Notes |
|---|---|---|
secret | string | Site secret. Backend only. |
token | string | From Nolgard.execute(). |
remoteIp | string, optional | Visitor IP from your connection or a proxy you trust. Never from form data. |
action | string, optional | Expected action. A mismatch fails with action-mismatch. |
account | {id?, email?}, optional | Your account ID and email. See Account signals. |
headers, authority | optional | Original inbound headers and Host, for AI agents. |
Response
{ "success": true, "verdict": "human", "score": 0, "decision": "allow",
"categories": [], "scores": { "automation": 0, "repeat": 0, "abuse": 0 }, "ref": "ev_8f2c1a", "action": "signup", "tokenAgeMs": 1840 }| Field | Type | Notes |
|---|---|---|
success | boolean | Token was valid, unexpired, unused and matched the site and action. A valid token does not mean the visitor is human. |
verdict | human | suspicious | bot | |
scores | { automation, repeat, abuse } | Integers 0–100. Bot signals, links to earlier accounts, and email/usage/referral/promo abuse, scored separately. See Risk axes and tuning. |
score | 0..100 | Higher means more likely automated. |
decision | allow | limit | challenge | shadow | block | What to do, based on the site mode. |
categories | string[] | Empty for clean humans. Values: automation, headless, script, spoofed, proxy, behavior, abuse, crawler, agent. |
ref | string | Event reference. Quote it when you contact support. |
action | string | null | Action the token was created for. |
tokenAgeMs | number | Token age at verification. |
quotaExceeded | true, optional | Present once you are over the monthly quota. |
test | true, optional | Present only for test keys. Never accept it in production. |
repeat | object, optional | See Account signals. |
email | {domain, disposable}, optional | Present when you send account.email. |
agent | object, optional | See AI agents. |
wouldDecide | decision, optional | Observe mode only: what block mode would have returned. |
Trial grants
Every verify response includes grant: { share, credits, requires }. Use grant.credits ?? grant.share as the trial budget. Share is 0..1. Credits is null until you set full trial credits in Settings → Trial; once set, it is floor(share × credits).
{ "grant": { "share": 0.2, "credits": 20, "requires": null } }Allow grants 100%. Limit grants the configured share (default 20%). Challenge, shadow, block and failures grant zero. Challenge requires step-up by default, payment if configured, or null if configured to block. Other decisions require nothing. Observe grants the full trial and includes wouldGrant from wouldDecide. Successful step-up verification returns its own grant: limited by default, or full if configured.
PATCH /api/sites/:key with { "trial": { "credits": 100, "limitShare": 0.2, "challenge": "step-up", "afterStepUp": "limited" } }. Credits: integer 0..1000000000 or null; limitShare: 0..1; challenge: step-up/payment/block; afterStepUp: full/limited.
Decisions & modes
| Decision | Your backend |
|---|---|
allow | Complete the requested action. |
limit | Reduce access, such as a shorter trial, fewer credits or no signup bonus. |
challenge | Ask for your own step-up, such as email confirmation or SMS. Nolgard has no CAPTCHA. |
shadow | Return your normal success response, but create nothing and send nothing. |
block | Reject with a generic error. |
Verdict × mode
| Verdict | Observe | Shadow | Block |
|---|---|---|---|
human | allow | allow | allow |
suspicious | allow | allow | challenge |
bot | allow | shadow | block |
| repeat account that is not a bot | allow | limit | limit |
Observe mode returns the block-mode decision in wouldDecide. A suspicious repeat account in block mode gets challenge.
Set each site's mode in the portal. Roll out in this order:
- Observe. Always returns
allow. CheckwouldDecidefor the block-mode decision. - Shadow. Returns
shadowfor bots andlimitfor repeat accounts. - Block. Returns
blockfor bots,limitfor repeat accounts andchallengefor borderline visitors.
Your backend must enforce shadow decisions. Nolgard cannot stop your app from sending a welcome email. Mode, origin and client-hint changes reach browsers within 15 seconds. Secret rotation is immediate.
Handling suspicious visitors
- At signup, create the account with a smaller trial or no bonus.
- Require email confirmation before issuing credits.
- Hold payouts and referral rewards for review.
- Do not tell the visitor why.
Start with 2 weeks in Observe. Watch wouldDecide and the portal. Label a few events, then switch to Shadow or Block.
Actions
Pass the same action to execute() and /v1/verify.
| Action | Advice |
|---|---|
signup | Send account.email. Use limit for trial size and credits. |
login | Send account.id. On challenge, ask for your own second factor. |
redeem_promo | Send account.id. On limit, skip the bonus. |
checkout | Call it before creating the Stripe session. Adds payment: { hold, threeDS }: Payments. |
When verify fails
On signup, fail open for timeouts and 5xx so real users can get in. For config errors (invalid-secret), fail closed and alert yourself.
let result;
try {
const response = await fetch('https://nolgard.com/v1/verify', { method: 'POST', headers, body, signal: AbortSignal.timeout(3000) });
if (response.status >= 500) return createAccount(); // 5xx: fail open
result = await response.json();
} catch {
return createAccount(); // timeout or network: fail open
}
if (result.error === 'invalid-secret') {
alertOnCall('Nolgard secret is wrong');
return verificationUnavailable(); // config error: fail closed
}AI agents
A user agent string alone does not verify an agent. Nolgard checks Web Bot Auth signatures or the operator's official IP ranges. A verified agent response looks like this:
"agent": { "name": "ChatGPT-User", "operator": "openai.com", "verified": true, "method": "web-bot-auth" }method is web-bot-auth, ip-range or null. Set a policy for verified agents on each site: allow, limit, shadow or block (the default). Unverified agents are treated like other automated visitors.
Signed requests to your own app
To check signatures sent to your server, forward the original inbound request headers and Host from your backend. Forward the headers you received; the headers your own outbound call sends are the wrong ones. Never take these values from browser JSON.
{ "secret": "SITE_SECRET", "token": "...", "remoteIp": "203.0.113.42",
"authority": "app.example.com",
"headers": {
"user-agent": "...",
"signature-agent": "...",
"signature-input": "...",
"signature": "..."
} }Event search
Enter space-separated terms in the portal search box. Terms are combined with AND. Quote values that contain spaces. Prefix a term with - to exclude it.
verdict:bot decision:shadow phase:verify action:signup
country:US asn:16509 category:headless ua:chrome label:human
score>70 score<=40 score=100
since:1h since:24h since:7d since:30d
-country:SE action:"newsletter signup"Keys: verdict, decision, phase, action, country, asn, category, ua, label, score, since. One time window per query, up to 30 days back. Unknown keys return an error instead of being ignored.
Alerts
Under the site's integrations, add a webhook, Slack, Telegram or email destination. Set the bot-share percentage and minimum event count. Checks run hourly. Each destination gets at most one alert every six hours. Email alerts go to your confirmed address.
{
"site": "pk_example",
"botShare": 75,
"total": 100,
"bot": 75,
"hour": "2026-10-21T10",
"url": "https://nolgard.com/app/#/sites/pk_example"
}The example above is the webhook body. hour is UTC. Slack receives a text message. Telegram receives chat_id and text. The test button sends zero counts.
Quotas
Free includes 1,000 new accounts per UTC calendar month across all your sites, no card and no time limit. A successful verify counts once when its account hash is first seen on that site. Without an account, every successful verify counts. Repeat logins, usage events, step-ups and failed verifies are not metered.
Pro and Business open when billing ships; these are the prices we intend to charge. Pro $49/month for 10,000 new accounts, then $0.006 each. Business $199/month for 50,000, then $0.004 each. Enterprise is custom above 100,000. If you expect more than 1,000 a month, ask for a 60 day pilot at no cost or request access.
At 100%, verification keeps working and adds quotaExceeded: true. At 200%, it returns HTTP 429 over-quota and leaves the token unused. Usage counts can lag by up to 30 seconds.
If verification is over quota or unreachable, let signups through unchecked (fail open). If the secret is wrong, stop and fix it (fail closed). See When verify fails.
Errors
Error responses use {"success": false, "error": "..."}.
| Error | Meaning | What to do |
|---|---|---|
invalid-input | Malformed request body. | Fix your request. |
invalid-secret | HTTP 401. Wrong secret. | Fail closed and fix the configuration. |
invalid-token | Token was not issued by Nolgard. | Reject. |
expired | Older than 5 minutes. | Reject. Fetch a new token in the browser on submit. |
replayed | Token was already used. | Reject. |
site-mismatch | Token belongs to another site. | Reject. |
action-mismatch | Token action differs from action. | Reject. The token is consumed. |
over-quota | HTTP 429. Twice the monthly quota used. | Apply your fail-open or fail-closed policy. |
Requests with a wrong secret are not logged or counted. Other failed verifications are logged but do not count toward usage.
Privacy
Nolgard checks network, browser and behaviour context on its servers. Raw IPs are not stored. Network identifiers are salted hashes. Events are deleted after 30 days. Data is stored in Cloudflare's EU jurisdiction.
If your jurisdiction requires consent before analytics, collect it before calling gate(). Do not put emails or other personal data in action names or URLs.
Nolgard will miss some bots and flag some people. Give real users a way through, such as a support contact or a challenge step.