Docs menu
SDKs

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

Shell
npm install https://inlineshield.io/downloads/sdk/inlineshield-node-1.0.0.tgz

Configure

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

Node.js
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"
});
OptionDefaultWhat it does
baseUrlhttps://api.inlineshield.ioThe API origin. INLINESHIELD_BASE_URL from the environment, if set.
timeoutMs3000Deadline of each request, in milliseconds: connecting, sending and reading the whole answer.
failOpentrueReturn an unavailable result (action "allow") instead of throwing when InlineShield is unavailable.
maxRetries1Retries 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.

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

Check 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).
Node.js
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.

Node.js
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.

Node.js
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.

Node.js
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.

Node.js
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 codeMeaning
NO_SECRETThe secret is empty or missing: the SDK never verifies with an empty key.
MALFORMED_HEADERThe header is not exactly t=<seconds>,v1=<64 lower-case hex characters>.
TIMESTAMP_OUT_OF_RANGEThe delivery was signed more than the tolerance (300 seconds by default) before or after now.
SIGNATURE_MISMATCHThe HMAC does not match: another secret, or a changed body.
INVALID_PAYLOADThe 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 errorWhen
TIMEOUTNo answer within the timeout.
UNAVAILABLENo connection, a server error (5xx), or an answer that is not JSON.
RATE_LIMIT_EXCEEDEDToo many requests (429).
TEST_LIMITA test key is over its daily test limit (429).
QUOTA_EXCEEDEDThe account is over its monthly quota (402, or QUOTA_EXCEEDED from siteverify).
A rate limit or a quota block also fails open. By default a 429 (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 classWhen
AuthErrorA missing, unknown, revoked or expired key (401), or a suspended account (403 TENANT_SUSPENDED). Always thrown.
ApiErrorA refused request: 400 BAD_REQUEST or INVALID_IP, 404 NOT_FOUND, 422 SCREENING_TOO_OLD, or a redirect (UNEXPECTED_REDIRECT). Always thrown.
UnavailableErrorCode TIMEOUT or UNAVAILABLE. Thrown only with failOpen: false.
RateLimitErrorCode RATE_LIMIT_EXCEEDED or TEST_LIMIT, with retryAfter in seconds. Thrown only with failOpen: false.
QuotaErrorCode QUOTA_EXCEEDED. Thrown only with failOpen: false.
WebhookVerificationErrorThrown 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.

Node.js
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).
}