Get started
Answer up to three questions. You get the exact steps for your stack, nothing else.
How it works
The SDK runs hardware fingerprinting in the browser and fetches an encrypted token. That token rides along with the request you want to protect. Your server posts it to the edge and gets back ALLOW, CHALLENGE, or BLOCK. The user sees nothing.
Step 2 is not optional. Without backend verification the token blocks nothing: bots pass while your dashboard fills with traffic. The WordPress plugin does both steps for you.
API Keys
TrustSig uses two keys: a public Site Key for your frontend, and a private Secret Key for your backend. Both belong to a project; find them in the project settings inside the dashboard.
Passed to the frontend SDK via data-site-key or the TrustSigProvider prop. Identifies your project. Cannot be used to verify tokens.
Used only on your server to verify tokens via the Edge API or the @trustsig/server SDK. Never expose in client-side code or commit to source control.
If a Secret Key leaks, open the project in the dashboard and use Rotate Keys. Rotation replaces both keys of that project at once; the old pair stops working immediately, so update your frontend data-site-key and your server environment variable together.
WordPress users: the free tier requires no keys at all. The plugin works out of the box with anonymous protection. API keys unlock the dashboard, analytics, and advanced rules.
Tokens are only issued to origins on the project's Allowed Domains list. See Projects & Domains for how the allow-list works and which local development hosts pass automatically.
Sign-in & Account Security
A TrustSig account can sign in with an email and password, with Google, or both. This page defines how the two methods combine, what two-factor authentication covers, who owns an organization, and exactly what account deletion removes.
Both methods target the same account, keyed by your email address. Signing in with Google for the first time on an existing, verified password account automatically connects Google to it; your password keeps working. You can also connect Google from dashboard account. The Google account's email must match your TrustSig email exactly.
| Action | Where | Requirement |
|---|---|---|
| Connect Google | Account page, or first Google sign-in | Google email matches your account email |
| Disconnect Google | Account page | A password must be set, so you cannot lock yourself out |
| Disable password sign-in | Account page | Google must be connected; confirmed with your password (and 2FA code if enabled) |
| Set or re-enable a password | Account page (Set password) or the login page's forgot-password flow | A secure link is emailed to you |
Disabling password sign-in removes the password credential entirely: password login is rejected, and stolen or reused passwords stop being a risk for the account. It is fully reversible through the Set password email.
TrustSig 2FA (authenticator app or email codes) applies to every sign-in, including Google. If 2FA is enabled, a Google sign-in is challenged for a code exactly like a password sign-in; connecting Google does not weaken or bypass it.
Our recommendation: if you sign in with Google only, your account security equals your Google account security. Keep 2FA enabled on your Google account and you do not need a separate TrustSig password or TrustSig 2FA. We do not recommend adding a password you will not use; a second credential is a second thing to phish. If you do use a password (alone or alongside Google), enable TrustSig 2FA with it.
Every account has a personal organization that owns its projects and subscription. Inviting members shares that organization; granting a member the Owner role gives them full permissions, but the organization itself, including billing, remains anchored to the founding account.
To hand an organization over for real, use Transfer ownership on the team page. Projects, members, pending invites, the subscription, and billing history move to the new owner; you stay on as an admin. Requirements: the new owner is an accepted member with a verified email, and their own personal organization is empty (no projects, members, or paid plan).
Deleting your account permanently removes your personal organization: its projects, their verification data, and your subscription (any paid plan is cancelled at Stripe). This cannot be undone.
Organizations you were invited to are not affected, whatever your role there. You are removed from their member lists; their projects, members, and billing continue unchanged. If your own organization still has members, you must remove them (or transfer ownership) before deletion is allowed.
Projects & Domains
A project is the unit everything else hangs off. Each project has its own Site Key and Secret Key, its own Allowed Domains list, and its own traffic stats and Troubleshoot log. You create projects in the dashboard; the keys are generated with the project.
A project maps to one application, not one hostname. If the same application is served from several hostnames (production, staging, a country domain), keep them in one project and add each hostname to its Allowed Domains list. All of them share the project's keys and report into the same stats.
Create separate projects for separate applications, or whenever you want isolation: separate keys (rotating one project's keys does not touch the others), separate traffic stats, and a separate Troubleshoot log per project.
Your plan sets two account-wide limits: how many projects you can create, and how many domains you can register in total across all projects. Both meters are shown on the projects page in the dashboard. Monthly request volume is a third, separate limit; see Usage & Quotas.
The edge only issues tokens to origins on the project's Allowed Domains list. Requests from any other origin are rejected and logged as NON_WHITELISTED_DOMAIN in the project's Troubleshoot log. Add every production and staging hostname you serve the SDK from.
Local development hosts are auto-allowed. You do not need to add them to your project. The following origins always pass the allow-list check:
- localhost and IPv6 loopback (::1, [::1])
- Link-local: IPv4 169.254.0.0/16, IPv6 fe80::/10
- RFC 1918 private IPv4: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
- Any hostname ending in .localhost, .local, or .test
So localhost:3000, 127.0.0.1, 192.168.1.42, mac-mini.local, and app.test all work during development without any dashboard changes. Production domains must still be added explicitly.
When the edge rejects a client, the rejection surfaces in the project's Troubleshoot log in the dashboard with one of these codes:
| Code | Meaning |
|---|---|
| NON_WHITELISTED_DOMAIN | The request origin is not on the project's Allowed Domains list. Add the hostname in the dashboard. |
| INVALID_SITE_KEY | The data-site-key the SDK sent does not match the project. Common after a key rotation with a stale frontend deploy. |
| CHALLENGE_EXPIRED | The token was older than its validity window when it reached verification. Usually a form left open for a long time before submitting. |
Usage & Quotas
Every plan includes a monthly volume of requests. Usage resets at the start of each billing period. This page defines exactly what consumes that volume and what happens when you run past it.
One client-side scan is one request. A scan is the SDK fingerprinting the browser and fetching a token from the edge. Server-side verification of that token is free: checking tokens never consumes quota, only issuing them does.
| Event | Counted |
|---|---|
Page view with data-auto-scan="true" (the initial scan) | 1 request |
| Form submit hooked by auto-scan (the SDK scans before each submit) | 1 request |
Each window.TrustSig.scan() call | 1 request |
getResponse() returning the cached token | 0, no new scan runs |
Backend verification (/verify, verifyRemote, verifyLocal) | 0, always free |
| Traffic on pages that do not load the SDK | 0 |
Usage is counted when the token is issued, whether or not your backend ever verifies it. That is also why the dashboard traffic numbers are labeled as tokens issued: they confirm the SDK is live, not that your server checked each one.
| Plan | Requests / month | Domains |
|---|---|---|
| Starter | 50,000 | 2 |
| Growth | 300,000 | 20 |
| Business | 750,000 | 25 |
| Enterprise | Unlimited | Custom |
Current prices and any plan changes live on the pricing page; your account's live limits are always shown in dashboard billing.
On paid plans, protection continues past the included volume as metered overage at €5 per 100,000 extra requests, up to the plan's overage allowance. An overage protection threshold in dashboard billing caps your exposure: if the unpaid overage balance exceeds the threshold you set (at most €100), service pauses until it is settled. One-time top-ups are also available if you prefer prepaid volume over metered overage.
Starter has no overage allowance. If you regularly exceed the included volume, upgrade the plan or reduce what you scan (below).
The dashboard overview shows the live monthly usage meter against your plan limit. Billing holds the usage history, the overage ledger, and invoices.
If only specific actions need protection, disable the automatic page-view scan with data-auto-scan="false" and trigger scans manually via the JavaScript API on the submits and logins you actually verify. You then spend exactly one request per protected action instead of one per page view.
1-minute setup.
Install from the official WordPress.org plugin directory, click Activate. Every form on your site is protected immediately, no code, no API keys, no configuration required.
Free tier protection requires no signup.1. Install the plugin
In your WordPress dashboard, go to Plugins › Add New, search for "TrustSig", and click Install Now. Or grab it directly from wordpress.org/plugins/trustsig-security.
2. Activate
Click Activate Plugin. That's it. TrustSig's invisible threat engine is now running on every form across your site.
What gets protected
- WordPress Login form
- User Registration form
- Password Reset form
- Comments form
- WooCommerce Checkout
- WooCommerce Login
- Contact Form 7
- Any custom form (auto-detected)
Script Tag (CDN)
The fastest way to protect any website. Paste one <script> tag into your <head> and TrustSig automatically protects all forms on the page.
This alone blocks nothing. It only attaches a token; your backend must verify it. Backend verification →
<script
src="https://edge.trustsig.eu/trustsig.js"
data-site-key="YOUR_SITE_KEY"
data-auto-scan="true"
></script>| Attribute | Description |
|---|---|
| data-site-key | Required. Your public project Site Key (pk_live_...). |
| data-auto-scan | Optional. Defaults to true. Set to false for manual triggers only. |
| data-intercept-requests | Optional. Auto-inject the token header on all fetch and XHR requests. |
If your site sends a Content-Security-Policy header, the browser blocks the SDK unless the TrustSig edge origin is allowed in three directives. Without them the script fails to load, no token is issued, and every protected request arrives without a trustsig-response.
Appended to an existing policy, the header looks like this:
script-src 'self' https://edge.trustsig.eu; frame-src https://edge.trustsig.eu; connect-src 'self' https://edge.trustsig.euIf you serve different environments from different hosts, the edge origin follows the environment: production uses edge.trustsig.eu. Keep existing sources in each directive and append the TrustSig origin; do not replace them.
Each scan the SDK runs counts as one request against your monthly quota; with auto-scan that is one per page view plus one per hooked form submit. See Usage & Quotas.
Auto Form Integration
With data-auto-scan="true", the SDK automatically hooks into all standard HTML form submissions. No custom JavaScript required.
Before each submit, the SDK runs its hardware fingerprint scan and injects a hidden trustsig-response field. Your backend receives this field automatically with the rest of the form data.
Receiving the field isn't verifying it. Your backend must POST trustsig-response to verify, otherwise bots pass. Backend verification →
<form action="/login" method="POST">
<input type="email" name="email" required />
<input type="password" name="password" required />
<!-- AUTOMATICALLY INJECTED BY SDK ON SUBMIT: -->
<!-- <input type="hidden" name="trustsig-response" value="EYJHBGCIOIJIUzI1..." /> -->
<button type="submit">Login</button>
</form>Read the token from req.body['trustsig-response'] on form POST, or from req.headers['x-trustsig-response'] when using data-intercept-requests with AJAX.
JavaScript API
For AJAX submissions, SPAs, or conditional UI states, TrustSig exposes a JavaScript API on the global window.TrustSig object after the SDK loads.
Listen for trustsig:ready on window to know when the initial token scan completes.
window.addEventListener('trustsig:ready', (e) => {
const { token, error } = e.detail;
if (token) {
document.getElementById('submit-btn').disabled = false;
} else {
// Fail-open: allow the action even if the scan failed
console.warn('TrustSig scan failed:', error);
}
});Use window.TrustSig.getResponse() for AJAX forms. Awaits any in-progress scan or returns the cached token immediately.
async function submitForm() {
const data = await window.TrustSig.getResponse();
const token = data?.token ?? null;
await fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-TrustSig-Response': token,
},
body: JSON.stringify({ email: 'user@example.com' }),
});
}Use window.TrustSig.scan({ key }) when the SDK is loaded without a data-site-key attribute, for example multi-tenant apps that resolve the key at runtime, or pages that conditionally protect only some actions. The call runs a fresh hardware-fingerprint scan against the supplied key and resolves with a token.
async function submitForm() {
const result = await window.TrustSig.scan({
key: 'pk_live_3426256ed11ff6e0f55af31c8328afe5',
});
const token = result?.token ?? null;
await fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-TrustSig-Response': token,
},
body: JSON.stringify({ email: 'user@example.com' }),
});
}Prefer getResponse() when the script tag already carries data-site-key; it returns the cached token from the SDK's auto-scan and avoids running a second fingerprint pass. Use scan({ key }) only when you need to supply the key at call time. Every scan counts as one request against your monthly quota; getResponse() on a cached token does not.
If your site sends a Content-Security-Policy header, the browser blocks the SDK unless the TrustSig edge origin is allowed in three directives. Without them the script fails to load, no token is issued, and every protected request arrives without a trustsig-response.
Appended to an existing policy, the header looks like this:
script-src 'self' https://edge.trustsig.eu; frame-src https://edge.trustsig.eu; connect-src 'self' https://edge.trustsig.euIf you serve different environments from different hosts, the edge origin follows the environment: production uses edge.trustsig.eu. Keep existing sources in each directive and append the TrustSig origin; do not replace them.
React / Next.js
The @trustsig/react package provides a Context Provider and hooks for seamless React and Next.js integration.
npm install @trustsig/reactWrap your app (or the subtree that needs protection) with TrustSigProvider.
"use client";
import { TrustSigProvider } from '@trustsig/react';
export default function RootLayout({ children }) {
return (
<TrustSigProvider siteKey="pk_live_..." autoScan={true}>
{children}
</TrustSigProvider>
);
}"use client";
import { useTrustSig } from '@trustsig/react';
export function LoginForm() {
const { getResponse } = useTrustSig();
const handleSubmit = async (e) => {
e.preventDefault();
const response = await getResponse();
if (response?.token) {
await fetch('/api/login', {
method: 'POST',
headers: { 'X-TrustSig-Response': response.token },
});
}
};
}Verify Endpoint
Once your server receives the trustsig-response token, POST it to the TrustSig edge network for verification. Compatible with any language or runtime. Verify calls are free; they do not count against your monthly quota.
https://edge.trustsig.eu/verifyapplication/json| Field | Description |
|---|---|
| secret | Your Secret Key (sk_live_...). Keep this server-side only. |
| token | The trustsig-response value from the form field or request header. |
app.post('/login', async (req, res) => {
const token =
req.body['trustsig-response'] ||
req.headers['x-trustsig-response'];
const result = await fetch('https://edge.trustsig.eu/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
secret: process.env.TRUSTSIG_SECRET_KEY,
token,
}),
});
const { action, score } = await result.json();
// Allow-list: ALLOW is the only value that proceeds. BLOCK, CHALLENGE,
// and any error all fail closed.
if (action !== 'ALLOW') {
return res.status(403).json({ error: 'Access denied.' });
}
// proceed with login logic
});Response Schema
The verify endpoint returns JSON. Gate access on action with an allow-list; it is the primary decision field.
| Field | Type | Description |
|---|---|---|
| action | String | ALLOW, CHALLENGE, or BLOCK. Primary decision field, gate access with an allow-list. |
| is_bot | Boolean | High-level verdict on synthetic actors. |
| score | Integer | Risk score from 0 to 100. Higher = more suspicious. Use for custom thresholds. |
| issued_at | Integer | UNIX timestamp of token creation. Use for replay detection. |
Recommended logic: fail closed with an allow-list, if (action !== 'ALLOW') deny(). This denies BLOCK, CHALLENGE, and any verification error. For fine-grained control, use score to apply custom thresholds, e.g. require additional verification above score 70 while still allowing score 50 to 70.
When verification fails, BotAnalysisResponse carries one of these in error (and mirrors it in factors):
| Code | Meaning |
|---|---|
| TOKEN_MISSING | No token supplied to the verify call. |
| TOKEN_EXPIRED | Token older than maxTokenAgeSeconds (default 300s). |
| TOKEN_REUSED | verifyLocal only. Token exceeded maxUsesPerToken (default 4) within its age window. |
| CRYPTO_FAIL | Decryption or signature check failed (tampered or wrong key). |
| API_FAIL | verifyRemote only. Edge call failed (network, non-200, timeout). |
| MALFORMED_RESPONSE | Edge response parsed but did not match the expected schema. |
@trustsig/server SDK
Node.js and edge-compatible SDK with built-in verification helpers. Supports local (zero-latency) and remote (full telemetry) verification modes.
npm install @trustsig/serverimport { TrustSig } from '@trustsig/server';
const ts = new TrustSig({ secretKey: process.env.TRUSTSIG_SECRET_KEY });
const token = request.headers.get('X-TrustSig-Response');
// Local verification: zero latency, no network call
const local = ts.verifyLocal(token);
if (local.action !== 'ALLOW') throw new Error('Access denied');
// Remote verification: full telemetry, recommended
const remote = await ts.verifyRemote(token);
if (remote.action !== 'ALLOW') throw new Error('Access denied');verifyLocal(token)Decrypts locally using ChaCha20Poly1305. Zero latency, no network calls. Best for high-throughput endpoints where every millisecond matters.
verifyRemote(token)Validates against the TrustSig Edge API. Returns full telemetry including score, signals, and geo data. Recommended for most use cases.
verifyLocal tracks the SHA-256 hash of each accepted token in process memory and caps reuse at 4 calls per token by default. The 5th call resolves to BLOCK with error: 'TOKEN_REUSED'. Cache entries auto-evict at issued_at + maxTokenAgeSeconds (default 300s), so memory stays bounded by the in-flight token set. Fail-closed contract is unchanged; never throws, always returns a BotAnalysisResponse.
| Option | Type | Default | Description |
|---|---|---|---|
| replayProtection | boolean | true | Track token hashes in process memory and cap reuse via verifyLocal. Set false in serverless runtimes. |
| maxUsesPerToken | number | 4 | Maximum times the same token may pass verifyLocal before TOKEN_REUSED. Ignored when replayProtection is false. |
| maxTokenAgeSeconds | number | 300 | Token age window. Also doubles as the cache eviction TTL for replay tracking entries. |
TrustSigErrorCode now includes 'TOKEN_REUSED':
TOKEN_MISSING | TOKEN_EXPIRED | TOKEN_REUSED | CRYPTO_FAIL | API_FAIL | MALFORMED_RESPONSEresult.factors also includes 'TOKEN_REUSED', so existing log and grouping pipelines pick it up automatically. The allow-list gate stays the same; if (result.action !== 'ALLOW') deny() already covers it.
// Defaults: replayProtection on, 4 uses per token.
const ts = new TrustSig({ secretKey });
// Single-use tokens (checkout confirmation, OTP, password change).
const oneShot = new TrustSig({ secretKey, maxUsesPerToken: 1 });
// Keep tracking but raise the cap.
const generous = new TrustSig({ secretKey, maxUsesPerToken: 50 });
// Off: accept the same token unlimited times within its age window.
const noReplay = new TrustSig({ secretKey, replayProtection: false });
// Manual reset (tests / operator action).
ts.clearReplayCache();Runtime requirement. The cache lives in process memory. It only enforces the cap inside a long-lived server: Node process, container, fly.io machine, Cloud Run with min-instances >= 1, queue worker, VPS. It is not preserved across cold starts and not shared between horizontally scaled instances.
In per-request serverless runtimes (each invocation spawns a fresh process) the cache always starts empty and the cap cannot be enforced. Pick one:
- replayProtection: false drops overhead, keeps decryption-only semantics.
- verifyRemote() is replay-resistant via edge nonce, no shared state needed.
- Share a cache externally (Redis, etc.) and call clearReplayCache() alongside your own pre-check.
Migration. No breaking changes. Existing verifyLocal callers keep working; the only new outcome is TOKEN_REUSED after >4 reuses of the same token in a 5-minute window. If your frontend resubmits the same token across multiple backend endpoints (e.g. one verify on the page + one on a separate XHR), confirm the total stays ≤ maxUsesPerToken or lift the cap. Fully serverless backends should set replayProtection: false so documented behaviour matches reality.