Castle SDK for Node
The official Node.js SDK for Castle. Castle analyzes user behavior in web and mobile apps to stop fraud before it happens.
This package is a thin, dependency-light wrapper around the Castle HTTP API. It exposes:
- Risk assessment —
POST /v1/risk,POST /v1/filter - Event logging —
POST /v1/log(fire-and-forget, no verdict) - Lists & List Items — full CRUD + search + count + batch
- Privacy / GDPR —
POSTandDELETE /v1/privacy/users(Article 15 & 17) - Events (enterprise) — search, group, and schema
- Webhook signature verification — validate
X-Castle-Signature - Secure mode — sign user IDs sent from the browser
A full list of supported events and the JSON shape of every payload is documented at https://reference.castle.io.
Requirements
- Node.js
>= 20(the SDK uses the built-in globalfetch/AbortSignal) - A Castle API secret
The package ships both ESM and CommonJS builds, so both import and require work out of the box.
Installation
npm install --save @castleio/sdk
yarn add @castleio/sdk
Quick start
import { Castle, ContextPrepareService } from '@castleio/sdk';
const castle = new Castle({ apiSecret: process.env.CASTLE_API_SECRET });
const context = ContextPrepareService.call(
req,
undefined,
castle.configuration
);
const verdict = await castle.risk({
type: '$login',
status: '$succeeded',
request_token: req.body.castle_request_token,
user: { id: '12345', email: '[email protected]' },
context,
});
switch (verdict.policy?.action) {
case 'deny':
// block the user
break;
case 'challenge':
// send 2FA / additional verification
break;
default:
// allow
}
With CommonJS:
const { Castle, ContextPrepareService } = require('@castleio/sdk');
ContextPrepareService.call(request, options, configuration) extracts the IP and headers that Castle needs from a Node request object. See Advanced configuration for how header allow/deny lists and proxy chains are resolved.
Configuration
The library is configured per Castle instance:
import { Castle, FailoverStrategy } from '@castleio/sdk';
const castle = new Castle({
apiSecret: process.env.CASTLE_API_SECRET,
// Automatic verdict from `risk`/`filter` when Castle is unreachable or
// times out. One of FailoverStrategy.allow (default), .deny, .challenge.
failoverStrategy: FailoverStrategy.allow,
// Request timeout in milliseconds before the failover strategy kicks in.
timeout: 1500,
// Logs Castle API requests and responses; must respond to `info`.
logger: console,
});
Config options
| Option | Type | Default | Description |
|---|---|---|---|
apiSecret | string | API secret from the dashboard. | |
timeout | number | 1500 | Time in ms before returning the failover strategy. |
failoverStrategy | FailoverStrategy | FailoverStrategy.allow | Automatic risk response on timeout: allow, deny, or challenge. |
logger | object | Logs Castle API requests/responses; must respond to info. | |
doNotTrack | boolean | false | When true, suppresses all requests and triggers failover on risk. Useful in development and testing. |
allowlisted | string[] | [] | Strict header allow-list (see Header allow/deny lists). |
denylisted | string[] | [] | Headers to always scrub, in addition to the always-blocked Cookie / Authorization. |
ipHeaders | string[] | [] | Custom headers to read the client IP from (see Client IP detection). |
trustedProxies | (string | RegExp)[] | [] | Known proxy IPs (strings or regexes). Pick this or trustedProxyDepth, never both. |
trustedProxyDepth | number | 0 | Number of known trusted proxies in the chain. Pick this or trustedProxies, never both. |
trustProxyChain | boolean | false | Trust the entire X-Forwarded-For chain. Warning: promiscuous — a malicious proxy can spoof the client IP. |
baseUrl | string | https://api.castle.io/v1 | Base Castle API URL. |
Multi-environment / multi-tenant
Each Castle instance carries its own configuration, so multiple environments or tenants are just multiple instances:
const castleTenantA = new Castle({
apiSecret: process.env.CASTLE_API_SECRET_TENANT_A,
});
const castleTenantB = new Castle({
apiSecret: process.env.CASTLE_API_SECRET_TENANT_B,
});
Usage
All endpoints are async methods on a Castle instance.
Risk
Evaluates high-risk events (logins, registrations, password resets, transactions). Returns a verdict (policy.action) plus risk scores and signals.
await castle.risk({
type: '$login',
status: '$succeeded',
request_token: req.body.castle_request_token,
user: { id: '12345', email: '[email protected]' },
context,
});
Filter
Blocks bots and bad traffic early in the chain (typically registration). Same response shape as Risk.
await castle.filter({
type: '$registration',
status: '$attempted',
request_token: req.body.castle_request_token,
params: { email: '[email protected]' },
context,
});
Log
Fire-and-forget event logging; no verdict is returned. Useful for events that should be visible in the dashboard but don't need a real-time decision.
await castle.log({
type: '$profile_update',
status: '$succeeded',
user: { id: '12345' },
context,
});
Lists & List Items
Lists let you organize users, IPs, transactions, or any custom property and use them in policies as allow/deny lists:
const list = await castle.createList({
name: 'Trusted IPs',
color: 'green',
primary_field: 'ip.address',
});
await castle.createListItem({
list_id: list.id,
primary_value: '1.2.3.4',
author: { type: '$analyst_email', identifier: '[email protected]' },
});
await castle.searchListItems({
list_id: list.id,
filters: [{ field: 'primary_value', op: '$eq', value: '1.2.3.4' }],
});
Available methods: createList, fetchAllLists, fetchList, updateList, deleteList, searchLists, createListItem, batchUpdateListItems, fetchListItem, searchListItems, countListItems, updateListItem, archiveListItem, unarchiveListItem.
Privacy (GDPR)
Supports GDPR Articles 15 (right of access) and 17 (right to be forgotten) via /v1/privacy/users. Both take an identifier and identifier_type ($id or $email):
await castle.requestUserData({
identifier: '[email protected]',
identifier_type: '$email',
});
await castle.deleteUserData({
identifier: 'user_42',
identifier_type: '$id',
});
Events (enterprise)
Query, group, and inspect the schema of stored events. Use queryEvents,
groupEvents, and eventsSchema:
await castle.queryEvents({
filters: [
/* ... */
],
});
await castle.groupEvents({
filters: [],
group_by: { fields: [] },
columns: [],
});
await castle.eventsSchema();
searchEvents and getEventsSchema are also available as aliases of
queryEvents and eventsSchema.
Webhook signature verification
Castle signs every webhook with an X-Castle-Signature header. Verify it against the raw request body before trusting the payload — verifyWebhookSignature throws a WebhookVerificationError on mismatch:
import express from 'express';
import { WebhookVerificationError } from '@castleio/sdk';
const app = express();
// The signature is computed over the raw body, so capture it as a Buffer.
app.post(
'/castle/webhooks',
express.raw({ type: 'application/json' }),
(req, res) => {
try {
castle.verifyWebhookSignature(req.body, req.get('x-castle-signature'));
} catch (err) {
if (err instanceof WebhookVerificationError) {
return res.sendStatus(400);
}
throw err;
}
const event = JSON.parse(req.body.toString());
// signature is valid; proceed
res.sendStatus(200);
}
);
Secure mode
Secure mode signs the user id server-side so it can't be tampered with in the browser. Pass the signature to the client-side castle.js SDK:
const signature = castle.secureModeSignature('user_123');
The signature is a hex HMAC-SHA256 of the user id keyed with your API secret.
Advanced configuration
The defaults are good for most deployments. The options below only matter if you have a non-trivial proxy chain or strict header policies.
Header allow/deny lists
By default the SDK sends every HTTP header except Cookie and Authorization. Castle uses these headers to fingerprint the request, so the broader the better.
import { DEFAULT_ALLOWLIST } from '@castleio/sdk';
const castle = new Castle({
apiSecret: process.env.CASTLE_API_SECRET,
// Always-blocked headers (in addition to Cookie/Authorization).
denylisted: ['X-Internal-Header'],
// Strict allow-list mode. Headers outside the list are sent with scrubbed
// values, except User-Agent which is always preserved. If you must use an
// allow list, we recommend starting from the curated default.
allowlisted: DEFAULT_ALLOWLIST,
});
Header names are case-insensitive and accept both _ and - as separators. A leading HTTP_ prefix is stripped automatically.
Client IP detection
Castle needs the original client IP, not the IP of your proxy or load balancer. The SDK reads X-Forwarded-For and Remote-Addr by default; pick one of the strategies below depending on your infrastructure:
const castle = new Castle({
apiSecret: process.env.CASTLE_API_SECRET,
// 1. Custom header (e.g. Cloudflare's Cf-Connecting-Ip).
ipHeaders: ['Cf-Connecting-Ip'],
// 2. Static, known proxy IPs.
trustedProxies: ['10.0.0.1'],
// 3. Ephemeral proxies but known chain depth.
trustedProxyDepth: 2,
// 4. Last resort: trust the entire X-Forwarded-For chain.
// Warning: vulnerable to header spoofing if a malicious proxy is in path.
trustProxyChain: false,
});
Pick either trustedProxies or trustedProxyDepth, never both. Private/loopback ranges in the exported TRUSTED_PROXIES constant are always considered trusted.
Errors
All HTTP/API exceptions inherit from APIError. ConfigurationError (setup problems) and WebhookVerificationError (webhook signature mismatch) extend Error directly. The most useful ones:
| Class | Raised when |
|---|---|
ConfigurationError | The SDK is misconfigured (missing API secret, bad URL). |
BadRequestError | 400 response. |
UnauthorizedError | 401 — bad API secret. |
ForbiddenError | 403 response. |
NotFoundError | 404 response. |
InvalidParametersError | 422 response with validation details. |
InvalidRequestTokenError | 422 — the request_token is missing or invalid. |
RateLimitError | 429 — back off and retry. |
PaymentRequiredError | 402 response. |
InternalServerError | 5xx response from Castle. |
WebhookVerificationError | A webhook signature did not match. |
The full list lives in src/errors.ts.
Upgrading to 3.0.0
3.0.0 is a modernization release. The public API (the Castle class and its
methods) is unchanged — most projects can upgrade without code changes.
What changed:
- Legacy endpoints removed.
authenticate,track, and the device endpoints (getDevice,getDevicesForUser,approveDevice,reportDevice) are gone — userisk,filter, andloginstead. The matching payload types (Payload,DevicePayload,UserDevicePayload,AuthenticateResult) were removed too.- If you previously used
reportDevice/approveDevice, manage device approvals and reports with the Lists API instead (e.g.createListItem/archiveListItem). See Reporting devices using Lists.
- If you previously used
- Node.js
>= 20is now required. The SDK uses the runtime's built-infetchandAbortSignalinstead ofnode-fetch/abort-controller, which are no longer dependencies. - Dual ESM + CommonJS build. The package now exposes a proper
exportsmap with separateimportandrequireentry points and type definitions. Deep imports intodist/were never supported and remain unsupported; import from the package root (@castleio/sdk). - The
lodash.*micro-dependencies were removed in favor of native JavaScript; this is internal and does not affect behavior.
If you previously relied on a custom overrideFetch, it still works — pass any
fetch-compatible implementation.
License
Available as open source under the terms of the MIT License.