NPM Package
The @surtai/guardian-web SDK runs Guardian's collect() flow in any modern browser. It gathers device signals, encrypts them locally, and returns an opaque payload your backend sends to Surt's evaluate endpoint. The SDK itself makes no network calls: there are no API keys in the browser, no preflight, no telemetry.
npm install @surtai/guardian-web
Unlike the native SDKs (iOS / Android / React Native), the web SDK does not expose a verify() method. All risk decisions happen server-side once your backend forwards the payload. See Collect (Server-to-Server) for the full backend flow.
Usage
import { collect } from '@surtai/guardian-web';
// Collects device data and returns an encrypted payload.
// No init, no API key, no network calls.
const { payload } = await collect({ collectLocation: false });
// Forward the payload to your backend.
await fetch('https://your-api.com/verify-device', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId: 'user_123', payload }),
});
That's the entire client-side API surface, one function, one option.
Options
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
collectLocation | boolean | No | false | When true, prompts the browser's Geolocation API. The only thing in collect() that can trigger a permission prompt. |
CollectResult
interface CollectResult {
/** Base64 payload. Pass as `payload.data` in the evaluate request shown below. */
payload: string;
}
Backend: forward to Surt
Your backend receives the payload from the browser and posts to Surt's evaluate endpoint:
POST https://api.surt.com/geolocation/transactions/evaluate
Content-Type: application/json
Authorization: Bearer YOUR_SURT_API_KEY
{
"customer_id": "user_123",
"transaction_type": "login",
"transaction_name": "Sign in",
"payload": {
"type": "encrypted",
"data": "<payload from collect()>"
},
"config": {
"response": {
"address": { "type": "include" }
}
}
}
The request shape, supported transaction_type values, config options, and response schema are identical across all Guardian SDKs. See Collect (Server-to-Server) for the complete reference, including Node / Java / Python examples.
Errors
collect() throws a single GuardianError for three conditions:
import { collect, GuardianError } from '@surtai/guardian-web';
try {
const { payload } = await collect();
} catch (err) {
if (err instanceof GuardianError) {
switch (err.code) {
case 'CRYPTO_UNAVAILABLE': /* not in a secure context */ break;
case 'ENCRYPTION_FAILED': /* encryption failed (rare) */ break;
case 'INVALID_OPTIONS': /* bad arguments */ break;
}
}
}
| Code | Meaning |
|---|---|
CRYPTO_UNAVAILABLE | window.crypto.subtle is missing. The SDK requires a secure context (HTTPS or localhost). |
ENCRYPTION_FAILED | The encryption step failed. Treat as a bug: capture and report. |
INVALID_OPTIONS | The argument to collect() is not an object. |
Individual collectors (fingerprint, battery, geolocation, network, etc.) never throw: they fail soft. A revoked permission or unsupported API just means the corresponding field is omitted from the payload. The backend tolerates sparse payloads.
Browser support
- Any evergreen browser with
window.crypto.subtle(Chrome, Edge, Firefox, Safari, Opera). - Secure context required: HTTPS in production,
localhostfor development. The SDK throwsCRYPTO_UNAVAILABLEoutside a secure context. - Geolocation, Battery Status, NetworkInformation, and other optional APIs degrade gracefully when unavailable.
Framework examples
- React
- Next.js
- Vue
- Svelte
- Vanilla JS
import { useState } from 'react';
import { collect } from '@surtai/guardian-web';
export function VerifyButton({ userId }: { userId: string }) {
const [loading, setLoading] = useState(false);
const handleClick = async () => {
setLoading(true);
try {
const { payload } = await collect({ collectLocation: false });
await fetch('/api/verify-device', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId, payload }),
});
} finally {
setLoading(false);
}
};
return (
<button onClick={handleClick} disabled={loading}>
{loading ? 'Verifying...' : 'Continue'}
</button>
);
}
'use client';
import { collect } from '@surtai/guardian-web';
export function SignInForm() {
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const { payload } = await collect();
await fetch('/api/sign-in', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: new FormData(e.currentTarget).get('email'),
payload,
}),
});
// Your /api/sign-in route forwards `payload` to Surt's /evaluate.
}
return (
<form onSubmit={handleSubmit}>
<input name="email" type="email" required />
<button type="submit">Sign in</button>
</form>
);
}
<script setup lang="ts">
import { ref } from 'vue';
import { collect } from '@surtai/guardian-web';
const loading = ref(false);
const props = defineProps<{ userId: string }>();
async function verify() {
loading.value = true;
try {
const { payload } = await collect();
await fetch('/api/verify-device', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId: props.userId, payload }),
});
} finally {
loading.value = false;
}
}
</script>
<template>
<button :disabled="loading" @click="verify">
{{ loading ? 'Verifying...' : 'Continue' }}
</button>
</template>
<script>
import { collect } from '@surtai/guardian-web';
export let userId;
let loading = false;
async function verify() {
loading = true;
try {
const { payload } = await collect();
await fetch('/api/verify-device', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId, payload }),
});
} finally {
loading = false;
}
}
</script>
<button on:click={verify} disabled={loading}>
{loading ? 'Verifying...' : 'Continue'}
</button>
<button id="verify">Continue</button>
<script type="module">
import { collect } from 'https://esm.sh/@surtai/guardian-web';
document.getElementById('verify').addEventListener('click', async () => {
const { payload } = await collect({ collectLocation: false });
await fetch('/api/verify-device', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId: 'user_123', payload }),
});
});
</script>
What's different from the native SDKs
@surtai/guardian-web | @surtai/guardian-rn / iOS / Android | |
|---|---|---|
verify() method | No | Yes |
collect() method | Yes (only API) | Yes |
| App-level initialization | None | initialize(apiKey, options) |
setCustomer() lifecycle | No | Yes |
| API key in client | No | Yes |
| Network calls from SDK | None | Yes |
| Hardware attestation | No (no browser equivalent) | Yes |
| Persists per-customer state | No (stateless per call) | Yes |
Web is intentionally lean: a single static function that produces an encrypted payload. Customer binding, transaction metadata, and decisions all happen in your backend.