Docs menu
Guides
API reference
Node.js SDK
The official Node.js package, @inlineshield/node: screen sign-ups, redeem browser tokens, send feedback, poll held sign-ups and verify webhooks, with typed results and errors.
Last updated
Install
@inlineshield/node 1.0.0 needs Node.js 18 or later and has no dependencies: it uses the built-in fetch. It ships ES modules and CommonJS, with TypeScript types. Until it is published on npm, install it from our download (inlineshield-node-1.0.0.tgz):
npm install https://inlineshield.io/downloads/sdk/inlineshield-node-1.0.0.tgzConfigure
Create one client with your secret API key and reuse it. Use a test key (bs_test_…) and the magic test addresses while you build: test screenings are not counted and send no webhooks. Without a key argument, the client reads INLINESHIELD_API_KEY from the environment; with no key at all it throws an auth error (MISSING_API_KEY).
import { Client } from '@inlineshield/node';
// Server-side only: your secret key (bs_live_… or bs_test_…) from the console, Settings › Keys.
const shield = new Client(process.env.INLINESHIELD_API_KEY, {
timeoutMs: 3000, // per request (the default)
failOpen: true, // the default: an outage gives an "unavailable" result with action "allow"
});| Option | Default | What it does |
|---|---|---|
baseUrl | https://api.inlineshield.io | The API origin. INLINESHIELD_BASE_URL from the environment, if set. |
timeoutMs | 3000 | Deadline of each request, in milliseconds: connecting, sending and reading the whole answer. |
failOpen | true | Return an unavailable result (action "allow") instead of throwing when InlineShield is unavailable. |
maxRetries | 1 | Retries after a failed connection (nothing was sent). |
The base URL must use https; plain http is accepted only for localhost, for local tests.
Methods
Results are the API's JSON as plain objects, with the API's field names (see the API reference), plus unavailable: false for a real answer, true for a fail-open result.
| Method | What it does |
|---|---|
verify({ email, ip, userAgent, username, clientDwellMs, honeypot }) | POST /api/v1/verify: screen a sign-up on the server. |
siteverify(token, email) | POST /api/v1/siteverify: redeem the browser script token. |
feedback(requestId | { token }, outcome, { category, note }) | POST /api/v1/feedback: report fraud or legit. |
getReview(reviewId) | GET /api/v1/reviews/{reviewId}: the status of a held sign-up. |
verifyWebhook(signatureHeader, rawBody, secret, toleranceSec = 300) | Check a webhook delivery and return its event. Also Client.verifyWebhook. |
Screen a sign-up
With the browser script on your form, redeem its token with siteverify before you create the account. When there is no token (NO_TOKEN), screen the sign-up on the server with verify instead. Any other error (INVALID_TOKEN, EXPIRED_TOKEN, TOKEN_ALREADY_USED, EMAIL_MISMATCH) means the token cannot be trusted: reject the sign-up. Then switch on action.
unavailable first. When InlineShield could not answer, the result has unavailable true, action "allow" (fail-open) and success false, because no token was redeemed, with the reason in error (for example TIMEOUT). Code that rejects every success false would reject all sign-ups during an outage. The order is: unavailable, then success, then error (NO_TOKEN: screen with verify; anything else: reject).import { Client } from '@inlineshield/node';
const shield = new Client(process.env.INLINESHIELD_API_KEY);
// In your sign-up handler (Express): the browser script put the token in the form.
async function screenSignup(req) {
const email = req.body.email;
const checked = await shield.siteverify(req.body.betshield_token, email);
if (checked.success || checked.unavailable) return checked;
if (checked.error === 'NO_TOKEN') {
// The script was blocked, timed out or is missing: screen on the server instead.
return shield.verify({ email, ip: req.ip, userAgent: req.get('user-agent') });
}
return { action: 'block', error: checked.error }; // INVALID_TOKEN, EXPIRED_TOKEN, TOKEN_ALREADY_USED, EMAIL_MISMATCH
}
const result = await screenSignup(req);
switch (result.action) {
case 'allow': /* create the account */ break;
case 'hold': /* create it pending and keep result.reviewId (live holds) */ break;
case 'block': /* reject the sign-up */ break;
}Server-only screening
Without the browser script, call verify with the player's email, IP and User-Agent from your incoming request. See Server-only integration.
import { Client } from '@inlineshield/node';
const shield = new Client(process.env.INLINESHIELD_API_KEY);
const result = await shield.verify({
email: req.body.email,
ip: req.ip, // the player's IP, not your server's
userAgent: req.get('user-agent'), // the player's browser
username: req.body.username, // optional
});
// result.action: "allow", "hold" or "block"; result.riskScore 0-100; result.reasons
if (result.unavailable) {
// InlineShield could not answer: action is "allow" (fail-open); result.error says why.
}Outcome feedback
Report what really happened (fraud or legit) so InlineShield learns from your outcomes. Feedback is not counted toward your quota. See Outcome feedback.
import { Client } from '@inlineshield/node';
const shield = new Client(process.env.INLINESHIELD_API_KEY);
// By the requestId InlineShield returned (accepted for screenings up to 180 days old):
const sent = await shield.feedback(requestId, 'fraud', { category: 'bonus_abuse', note: 'Chargeback on first deposit' });
// Or by the browser token, for about a day after the screening:
await shield.feedback({ token }, 'legit', { category: 'good_customer' });
// sent.changed is false when the same feedback was already recorded.Review status
A live hold carries a reviewId. The signup.approved and signup.rejected webhooks tell you the decision; this call is the fallback. Test holds have no reviewId. See Hold and review.
import { Client } from '@inlineshield/node';
const shield = new Client(process.env.INLINESHIELD_API_KEY);
// The reviewId of a held sign-up. Webhooks tell you first; this is the fallback.
const review = await shield.getReview(reviewId);
if (review.status === 'approved') {
// activate the account
} else if (review.status === 'rejected') {
// delete or disable it (review.decisionReason: "operator" or "expired")
}Verify webhooks
Verify each delivery with the raw request body, before you parse it. The SDK computes HMAC-SHA256 of <t>.<raw body> with your endpoint's whole secret (whsec_…), compares it with v1 in constant time, refuses a t more than 300 seconds before or after now (the last argument changes the tolerance), and returns the parsed event. Pass the body as a Buffer: with Express, use express.raw() on the webhook route, never express.json(). See Webhooks.
import { verifyWebhook, WebhookVerificationError } from '@inlineshield/node';
// Express: app.post('/inlineshield/webhook', express.raw({ type: '*/*' }), handleWebhook);
// express.raw keeps the exact bytes InlineShield signed: do not use express.json() on this route.
function handleWebhook(req, res) {
let event;
try {
event = verifyWebhook(req.get('X-InlineShield-Signature'), req.body, process.env.INLINESHIELD_WEBHOOK_SECRET);
} catch (err) {
if (err instanceof WebhookVerificationError) return res.status(400).send(err.code);
throw err;
}
if (event.event === 'signup.approved') {
// activate the account that has reviewId event.data.reviewId
} else if (event.event === 'signup.rejected') {
// delete or disable it (event.data.decisionReason)
}
// Deliveries can repeat: make this idempotent (key on event.id).
res.sendStatus(200);
}| Error code | Meaning |
|---|---|
NO_SECRET | The secret is empty or missing: the SDK never verifies with an empty key. |
MALFORMED_HEADER | The header is not exactly t=<seconds>,v1=<64 lower-case hex characters>. |
TIMESTAMP_OUT_OF_RANGE | The delivery was signed more than the tolerance (300 seconds by default) before or after now. |
SIGNATURE_MISMATCH | The HMAC does not match: another secret, or a changed body. |
INVALID_PAYLOAD | The signature matches but the body is not a JSON object. |
Errors
Fail-open is the default. When InlineShield cannot be reached, does not answer in time, answers with a server error, a rate limit or a quota block, the methods do not throw: they return an "unavailable" result with unavailable true and the reason in error. For verify and siteverify that result carries action "allow", so a sign-up goes through during an outage; getReview answers status "pending". Set failOpen to false to get exceptions instead. A refused key and a refused request are always thrown. Only a failed connection, where nothing was sent, is retried (once, by default); a timeout or an answer is never retried, so a screening is never recorded twice.
| unavailable result error | When |
|---|---|
TIMEOUT | No answer within the timeout. |
UNAVAILABLE | No connection, a server error (5xx), or an answer that is not JSON. |
RATE_LIMIT_EXCEEDED | Too many requests (429). |
TEST_LIMIT | A test key is over its daily test limit (429). |
QUOTA_EXCEEDED | The account is over its monthly quota (402, or QUOTA_EXCEEDED from siteverify). |
RATE_LIMIT_EXCEEDED, TEST_LIMIT) or an exhausted quota (QUOTA_EXCEEDED) gives action "allow", so every further sign-up is admitted unscreened until the limit resets; a flood that exceeds your limit gets through. To choose otherwise, check result.error on an unavailable result and hold those sign-ups for review or reject them, or set failOpen to false and handle the rate-limit and quota errors yourself.| Error class | When |
|---|---|
AuthError | A missing, unknown, revoked or expired key (401), or a suspended account (403 TENANT_SUSPENDED). Always thrown. |
ApiError | A refused request: 400 BAD_REQUEST or INVALID_IP, 404 NOT_FOUND, 422 SCREENING_TOO_OLD, or a redirect (UNEXPECTED_REDIRECT). Always thrown. |
UnavailableError | Code TIMEOUT or UNAVAILABLE. Thrown only with failOpen: false. |
RateLimitError | Code RATE_LIMIT_EXCEEDED or TEST_LIMIT, with retryAfter in seconds. Thrown only with failOpen: false. |
QuotaError | Code QUOTA_EXCEEDED. Thrown only with failOpen: false. |
WebhookVerificationError | Thrown by verifyWebhook. |
Every class extends InlineShieldError, with code (the error code) and status (the HTTP status, or null when there was no answer). Error messages never contain your API key or an email address, and the SDK logs nothing. Every API error code is on Errors and HTTP codes.
import { Client, AuthError, ApiError, UnavailableError, RateLimitError, QuotaError } from '@inlineshield/node';
const shield = new Client(process.env.INLINESHIELD_API_KEY);
let result;
try {
result = await shield.verify({ email: req.body.email, ip: req.ip, userAgent: req.get('user-agent') });
if (result.unavailable) {
// Fail-open: result.error is TIMEOUT, UNAVAILABLE, RATE_LIMIT_EXCEEDED, TEST_LIMIT or QUOTA_EXCEEDED.
}
} catch (err) {
if (err instanceof AuthError) {
// INVALID_API_KEY, EXPIRED_API_KEY, TENANT_SUSPENDED…: fix the configuration (thrown even with failOpen).
} else if (err instanceof ApiError) {
// The request was refused: err.code (e.g. INVALID_IP) and err.status.
}
throw err;
}
// Fail-closed instead: with failOpen: false an outage throws.
const strict = new Client(process.env.INLINESHIELD_API_KEY, { failOpen: false });
try {
await strict.verify({ email: req.body.email, ip: req.ip, userAgent: req.get('user-agent') });
} catch (err) {
if (!(err instanceof UnavailableError || err instanceof RateLimitError || err instanceof QuotaError)) throw err;
// InlineShield is unavailable: reject the sign-up, or ask the player to try again (err.retryAfter on a rate limit).
}