Backend Integration
Use the backend integration when a signup, login, checkout, account change, or API request needs a trusted decision. The browser supplies a fingerprintId; your server uses an authenticated API lookup to retrieve the risk record and combines it with your own business context.
Do not make a high-impact decision from a browser-visible score alone. Browser responses can intentionally suppress numeric verdict fields, and anything in browser code can be altered by the visitor.
Overview
The supported lookup flow is:
- Browser: collect with the script tag or
@noxtica/sdk. - Browser: read the returned
fingerprintId, whether the browser result is scored or suppressed. - Your application: submit that identifier with the business action to your own backend.
- Backend: call
GET /v1/device/:deviceIdwith a secret API key scoped toread:device. - Backend: distinguish found, missing, unauthorized, rate-limited, and unavailable outcomes.
- Policy: start by logging the proposed action; add step-up or block behavior only after reviewing results against your own outcomes.
Step 1: Embed the Collector
Place the optional session-replay head helper first in <head>, then load the collector asynchronously. data-auto-check-once is required here so auto-init actually requests a cache-respecting assessment.
<script
src="https://collect.noxtica.com/collector/noxtica-recorder-head.d9ae068e.js"
integrity="sha384-0aarRZ9aHZq4csV3R+MzJUTnerNzrddznWNV/KoKPkKp2Qh40kGsrFiPiMAuvmg9"
crossorigin="anonymous"
></script>
<script
src="https://collect.noxtica.com/collector/noxtica.js"
data-site-key="pk_prod_your_site_key_here"
data-auto-init
data-auto-check-once
async
></script>
The first script is needed only for early session-replay coverage. It does not transmit by itself, and replay remains subject to eligibility, policy, browser privacy signals, sampling, and consent or other approved basis. See Getting Started for strict CSP, SRI upgrades, tag managers, and consent details.
Or collect manually:
const client = NoxticaCollector.createClient({
siteKey: 'pk_prod_your_site_key_here',
});
const result = await client.checkOnce();
console.log('Fingerprint ID:', result.fingerprintId);
if (typeof result.score === 'number' && result.risk_level) {
console.log('Browser-visible risk:', result.risk_level, result.score);
} else {
console.log('Browser verdict suppressed:', result.status);
}
A suppressed browser verdict can still provide the fingerprintId needed for an authenticated backend lookup. A thrown error or missing identifier is different: no usable fresh identifier was returned.
Step 2: Store the Fingerprint ID
Send the identifier to your backend with the action it will inform. Do not put it in a URL.
const result = await client.checkOnce();
if (!result.fingerprintId) {
throw new Error('No fingerprint ID is available for server lookup');
}
await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: userEmail,
password: userPassword,
deviceId: result.fingerprintId,
}),
});
On your backend, associate deviceId with the current request or authenticated session only after applying your normal authentication and input validation:
app.post('/api/login', async (req, res) => {
const { email, password, deviceId } = req.body;
const user = await authenticate(email, password);
if (!user) {
return res.status(401).json({ error: 'invalid_credentials' });
}
req.session.deviceId = deviceId;
return res.json({ ok: true });
});
A device identifier is not proof of identity. Do not use it as an authentication token or permanent person identifier.
Step 3: Create a Server API Key
In Backoffice:
- Open API Config.
- Create a key for the backend service that will perform lookups.
- Grant only
read:devicefor the endpoint in this guide. - Save the secret when it is shown and place it in your server-side secret manager.
Server API keys use an sk_... form. Never embed one in browser JavaScript, HTML, mobile application resources, URLs, or client-visible logs. Rotate or revoke a key according to your operational policy when its ownership changes or exposure is suspected.
Step 4: Look Up Device Data
The lookup endpoint is GET /v1/device/:deviceId:
const NOXTICA_API = 'https://collect.noxtica.com';
const SECRET_KEY = process.env.NOXTICA_API_KEY;
async function lookupDevice(deviceId) {
const response = await fetch(`${NOXTICA_API}/v1/device/${encodeURIComponent(deviceId)}`, {
headers: {
Authorization: `Bearer ${SECRET_KEY}`,
},
});
if (response.status === 404) {
// New, expired, deleted, or not present in this tenant.
return null;
}
if (response.status === 401) {
throw new Error('Noxtica API key is missing or invalid');
}
if (response.status === 403) {
throw new Error('Noxtica API key lacks read:device');
}
if (response.status === 429) {
const error = new Error('Noxtica device lookup rate limited');
error.retryAfter = response.headers.get('Retry-After');
throw error;
}
if (!response.ok) {
throw new Error(`Noxtica device lookup unavailable: ${response.status}`);
}
return response.json();
}
Example scored response:
{
"ok": true,
"device": {
"deviceId": "abc123...",
"domainId": "d_xyz...",
"score": 25,
"riskLevel": "low",
"confidence": 0.6,
"flags": [],
"reasonCodes": [],
"country": "US",
"city": "San Francisco",
"createdAt": "2025-01-01T00:00:00.000Z",
"lastSubmittedAt": "2025-01-05T12:00:00.000Z",
"lastSeenAt": "2025-01-07T08:30:00.000Z"
}
}
Some additive fields depend on record age, tenant eligibility, and the assessment that ran. Write consumers to tolerate null, empty, and additional fields rather than treating absence as a clean result.
Step 5: Use the Data
Keep transport state separate from risk. The following example proposes an application action without turning a missing or failed lookup into low:
async function proposeTransactionAction(deviceId) {
let response;
try {
response = await lookupDevice(deviceId);
} catch (error) {
return {
action: 'step_up',
reason: 'risk_lookup_unavailable',
error,
};
}
if (!response?.device) {
return {
action: 'step_up',
reason: 'device_unknown',
};
}
const { riskLevel, score, confidence } = response.device;
if (typeof score !== 'number' || typeof confidence !== 'number' || !riskLevel) {
return {
action: 'review',
reason: 'risk_record_incomplete',
};
}
if (riskLevel === 'critical' || riskLevel === 'high') {
return {
action: 'step_up',
reason: 'elevated_device_risk',
score,
confidence,
};
}
if (riskLevel === 'medium') {
return {
action: 'observe',
reason: 'review_device_risk',
score,
confidence,
};
}
return {
action: 'allow',
reason: 'limited_device_risk_evidence',
score,
confidence,
};
}
This is a policy shape, not a universal threshold recommendation. Pair the device result with your own account age, authentication state, transaction value, abuse history, and protected operation. Start with observe, capture the proposed action, and compare it with known outcomes before introducing friction.
API Reference
GET /v1/device/:deviceId
Looks up one device record within the API key’s tenant.
Authentication: Authorization: Bearer sk_...
Required scope: read:device
Response fields:
| Field | Type | Customer-visible meaning |
|---|---|---|
deviceId | string | Device assessment identifier. |
domainId | string | Domain associated with the record. |
score | number | Risk score from 0 to 100 when the stored record is scored. |
riskLevel | string | minimal, low, medium, high, or critical. |
confidence | number | Confidence attached to this assessment. |
flags | string[] | Detailed server-visible risk flags, when present. |
reasonCodes | string[] | Published reason codes, when available for the tenant and record. |
subscores | object or null | Category-level score information, when available. |
details | object or null | Additional server-visible explanation, when available. |
country | string or null | Country context associated with the last assessment. |
city | string or null | City context associated with the last assessment. |
createdAt | string | First-seen timestamp. |
updatedAt | string | Last record-update timestamp. |
lastSubmittedAt | string | Last full assessment timestamp. |
lastSeenAt | string | Last observed timestamp. |
Do not write policy against undocumented fields or assume optional explanation fields are always populated.
Error responses:
| Status | Meaning | Application handling |
|---|---|---|
401 | Missing or invalid API key. | Fix credentials; do not retry as low risk. |
403 | Key lacks read:device. | Fix the key scope. |
404 | No current record in this tenant. | Treat as unknown; choose your new-device policy. |
429 | Read limit exceeded. | Honor Retry-After; use your unavailable policy. |
5xx | Service or configuration error. | Apply your unavailable policy and retain request context for troubleshooting. |
Risk Levels Reference
| Score range | Level | Suggested evaluation posture |
|---|---|---|
| 0–19 | minimal | Allow or observe, subject to your other controls. |
| 20–39 | low | Allow or observe; limited risk evidence is not identity proof. |
| 40–59 | medium | Observe or review before adding friction. |
| 60–79 | high | Consider step-up when confidence and business context support it. |
| 80–100 | critical | Consider the strongest reviewed response for that surface. |
These bands describe a scored record only. A missing record, request failure, malformed response, or suppressed browser verdict has no numeric score and must not be placed in the minimal or low band.
Best Practices
Store deviceId securely
Store a fingerprintId only where the decision workflow needs it:
- in your server-side session;
- in a tenant-scoped database record linked to the relevant operation; or
- in a protected audit event with an appropriate retention period.
Do not expose it in URLs, use it as a login credential, or send it to unrelated analytics systems.
Handle missing devices gracefully
A 404 can represent a new, expired, deleted, or tenant-mismatched record. Model that as unknown, not low:
const result = await lookupDevice(deviceId);
if (!result) {
return {
riskState: 'unknown',
action: 'step_up',
reason: 'device_not_found',
};
}
Choose the fallback per operation. A read-only page, a password reset, and a high-value transaction may need different responses.
Respect rate limits
Do not hard-code an assumed limit. On 429, honor the Retry-After header, stop tight retry loops, and record enough request context to investigate volume. Authentication and scope errors require configuration changes, not retries.
Cache lookup results
Short-lived application caching can reduce repeated reads within one decision window. Scope cache entries by the device identifier and your own tenant or environment boundary, cap the cache, and never extend the risk record beyond the freshness your policy accepts.
const cache = new Map();
const CACHE_TTL_MS = 60_000;
async function getCachedDevice(deviceId) {
const cached = cache.get(deviceId);
if (cached && Date.now() < cached.expiresAt) {
return cached.value;
}
const value = await lookupDevice(deviceId);
if (value) {
cache.set(deviceId, {
value,
expiresAt: Date.now() + CACHE_TTL_MS,
});
}
return value;
}
Use a bounded production cache rather than an ever-growing process Map; the example shows the freshness rule only.
Other API Endpoints
The Server API also exposes operational views. Grant their distinct scopes only to services that need them:
| Endpoint | Method | Scope | Customer task |
|---|---|---|---|
/v1/devices | GET | read:fingerprints | List and filter device records. |
/v1/events | GET | read:events | Review submission events. |
/v1/visits | GET | read:visits | Review visit activity. |
/v1/exports/fingerprints | GET | read:export | Export device data for governed analysis. |
Use the Features Reference to understand the corresponding operator workflows. For policy behavior, see Browser Security and run it in observation mode first.
Next Steps
- Review the full Integration Flow and its evaluate-before-enforce workflow.
- Use Browser Runtime to diagnose missing IDs, suppressed results, CSP, and replay state.
- Open the Backoffice to compare browser events, server lookups, and operator records.
- Browse the Features Reference for customer operations and capability eligibility.