StartGet started
Documentation

Get started

Answer up to three questions. You get the exact steps for your stack, nothing else.

1 What are you protecting?

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.

1
Frontend token
The SDK fingerprints the client, fetches a token, and attaches it to the form submit or API call.
2
Backend verification
Your server verifies the token against the edge and proceeds only on the ALLOW verdict.

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.

Public, safe to expose
pk_live_...

Passed to the frontend SDK via data-site-key or the TrustSigProvider prop. Identifies your project. Cannot be used to verify tokens.

Private, keep secret
sk_live_...

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.

Rotating keys

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.

Password and Google sign-in

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.

ActionWhereRequirement
Connect GoogleAccount page, or first Google sign-inGoogle email matches your account email
Disconnect GoogleAccount pageA password must be set, so you cannot lock yourself out
Disable password sign-inAccount pageGoogle must be connected; confirmed with your password (and 2FA code if enabled)
Set or re-enable a passwordAccount page (Set password) or the login page's forgot-password flowA 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.

Two-factor authentication and Google accounts

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.

Organization ownership and transfer

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).

What account deletion removes

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.

One project or several?

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.

Plan limits

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.

Allowed Domains (origin allow-list)

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.

Troubleshooting rejected requests

When the edge rejects a client, the rejection surfaces in the project's Troubleshoot log in the dashboard with one of these codes:

CodeMeaning
NON_WHITELISTED_DOMAINThe request origin is not on the project's Allowed Domains list. Add the hostname in the dashboard.
INVALID_SITE_KEYThe data-site-key the SDK sent does not match the project. Common after a key rotation with a stale frontend deploy.
CHALLENGE_EXPIREDThe 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.

What counts as a request

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.

EventCounted
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() call1 request
getResponse() returning the cached token0, no new scan runs
Backend verification (/verify, verifyRemote, verifyLocal)0, always free
Traffic on pages that do not load the SDK0

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.

Included volume per plan
PlanRequests / monthDomains
Starter50,0002
Growth300,00020
Business750,00025
EnterpriseUnlimitedCustom

Current prices and any plan changes live on the pricing page; your account's live limits are always shown in dashboard billing.

Overage

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).

Monitoring usage

The dashboard overview shows the live monthly usage meter against your plan limit. Billing holds the usage history, the overage ledger, and invoices.

Reducing usage

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.

WordPress

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.
Install from WordPress.org

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 →

index.html
<script
  src="https://edge.trustsig.eu/trustsig.js"
  data-site-key="YOUR_SITE_KEY"
  data-auto-scan="true"
></script>
Configuration Attributes
AttributeDescription
data-site-keyRequired. Your public project Site Key (pk_live_...).
data-auto-scanOptional. Defaults to true. Set to false for manual triggers only.
data-intercept-requestsOptional. Auto-inject the token header on all fetch and XHR requests.
Content Security Policy

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.

script-srcloads trustsig.jshttps://edge.trustsig.eu
frame-srcrenders the invisible challenge framehttps://edge.trustsig.eu
connect-srcfetches the encrypted tokenhttps://edge.trustsig.eu

Appended to an existing policy, the header looks like this:

Content-Security-Policy
script-src 'self' https://edge.trustsig.eu; frame-src https://edge.trustsig.eu; connect-src 'self' https://edge.trustsig.eu

If 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 →

login.html
<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.

trustsig:ready event

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);
  }
});
Manual token retrieval

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' }),
  });
}
One-shot scan with inline key

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.

Content Security Policy

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.

script-srcloads trustsig.jshttps://edge.trustsig.eu
frame-srcrenders the invisible challenge framehttps://edge.trustsig.eu
connect-srcfetches the encrypted tokenhttps://edge.trustsig.eu

Appended to an existing policy, the header looks like this:

Content-Security-Policy
script-src 'self' https://edge.trustsig.eu; frame-src https://edge.trustsig.eu; connect-src 'self' https://edge.trustsig.eu

If 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/react
1. Add the Provider

Wrap your app (or the subtree that needs protection) with TrustSigProvider.

app/layout.tsx
"use client";
import { TrustSigProvider } from '@trustsig/react';

export default function RootLayout({ children }) {
  return (
    <TrustSigProvider siteKey="pk_live_..." autoScan={true}>
      {children}
    </TrustSigProvider>
  );
}
2. Use the hook
"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.

Endpoint details
Endpointhttps://edge.trustsig.eu/verify
MethodPOST
Content-Typeapplication/json
Request body
FieldDescription
secretYour Secret Key (sk_live_...). Keep this server-side only.
tokenThe trustsig-response value from the form field or request header.
Node.js example
server.js
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.

FieldTypeDescription
actionStringALLOW, CHALLENGE, or BLOCK. Primary decision field, gate access with an allow-list.
is_botBooleanHigh-level verdict on synthetic actors.
scoreIntegerRisk score from 0 to 100. Higher = more suspicious. Use for custom thresholds.
issued_atIntegerUNIX 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.

Error codes (@trustsig/server)

When verification fails, BotAnalysisResponse carries one of these in error (and mirrors it in factors):

CodeMeaning
TOKEN_MISSINGNo token supplied to the verify call.
TOKEN_EXPIREDToken older than maxTokenAgeSeconds (default 300s).
TOKEN_REUSEDverifyLocal only. Token exceeded maxUsesPerToken (default 4) within its age window.
CRYPTO_FAILDecryption or signature check failed (tampered or wrong key).
API_FAILverifyRemote only. Edge call failed (network, non-200, timeout).
MALFORMED_RESPONSEEdge 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/server
Usage
import { 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');
Verification methods
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.

Replay protection (verifyLocal)

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.

OptionTypeDefaultDescription
replayProtectionbooleantrueTrack token hashes in process memory and cap reuse via verifyLocal. Set false in serverless runtimes.
maxUsesPerTokennumber4Maximum times the same token may pass verifyLocal before TOKEN_REUSED. Ignored when replayProtection is false.
maxTokenAgeSecondsnumber300Token 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_RESPONSE

result.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.

replay-protection.ts
// 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.