Server SDKs
About Composer Server SDKs
Spreedly Composer enables developers to decouple security logic from their application code. This has several benefits including:
- The ability to toggle and compare security services like fraud engines, MFA, KYC, and bot prevention.
- Faster responses to new attacks. When threats evolve and new vulnerabilities are identified, your application's security logic can be updated without changing a single line of code.
- The ability to put in placeholders for future security improvements while focusing on product development.
- A way to visualize all application security logic in one place.
The Server SDK is the primary interface for interacting with Composer. It calls checkpoints at key moments of risk in your application and helps you interpret the responses.
Each server SDK lets you call a checkpoint, interpret the response with the helper methods (isAllowed, isRunning, isDenied, and so on), and track events. The examples and reference tables on this page use the Node.js SDK. Option names and defaults can differ in other languages, so check your SDK's README.
Prerequisites
You will need to obtain a secret API key for your application from the Dodgeball developer center. In-app setup instructions are available in the Dodgeball Dashboard under Developer Center > API Configuration. If you don't already have an account, contact support.
Related
Server SDKs work alongside the JavaScript Client SDK, which runs in the browser and supplies the source token each checkpoint uses.
Migrating an existing integration? See From Dodgeball or From Composer (legacy).
For runnable reference apps, see composer-examples. It includes a server example in every supported language and a Next.js client example that uses the JavaScript Client SDK.
Available SDKs
| Language | Package | README |
|---|---|---|
| Node.js | @spreedly/composer-sdk-server (npm) | composer-sdk-server-node |
| Python | spreedly-composer-sdk-server (PyPI) | composer-sdk-server-python |
| Ruby | spreedly-composer-sdk-server (RubyGems) | composer-sdk-server-ruby |
| Go | github.com/spreedly/composer-sdk-server-go | composer-sdk-server-go |
| PHP | spreedly/composer-sdk-server (Packagist) | composer-sdk-server-php |
| .NET | Spreedly.ComposerSdk.Server (NuGet) | composer-sdk-server-dotnet |
| Java | com.spreedly.composer:composer-sdk-server (Maven) | composer-sdk-server-java |
Installation
Use npm to install the Node.js SDK:
npm install @spreedly/composer-sdk-serverAlternatively, using yarn:
yarn add @spreedly/composer-sdk-serverConfiguration
Initialize the SDK with your secret API key.
import { Composer } from '@spreedly/composer-sdk-server';
const composer = new Composer(process.env.SPREEDLY_SECRET_KEY, {
apiUrl: 'https://api.sandbox.dodgeballhq.com',
logLevel: 'ERROR',
requestTimeoutMs: 5000
});| Option | Default | Description |
|---|---|---|
apiUrl | https://api.dodgeballhq.com | The base URL of the Composer API. Use https://api.sandbox.dodgeballhq.com for the sandbox environment. |
logLevel | INFO | TRACE, INFO, ERROR, or NONE. At INFO the SDK logs no request or response payloads. TRACE logs payloads with end-user fields such as ip and data replaced by [REDACTED]. |
requestTimeoutMs | 30000 | The maximum time, in milliseconds, for a single HTTP request. |
These are the Node.js options. For other languages, see your SDK's README.
Usage
At a moment of risk, such as placing an order, call a checkpoint and act on the result:
import { Composer } from '@spreedly/composer-sdk-server';
import express from 'express';
import { isIP } from 'net';
const app = express();
app.use(express.json());
const composer = new Composer(process.env.SPREEDLY_SECRET_KEY);
// Client IP for the risk engine. Callers must not choose this value.
// Default (TRUSTED_PROXY_COUNT=0): socket address, ignore X-Forwarded-For.
// Positive count: that many hops from the right of X-Forwarded-For; the hop must be an IP.
// Set TRUSTED_PROXY_COUNT only if your app runs behind trusted reverse proxies.
const getIp = (req) => {
const hops = Number(process.env.TRUSTED_PROXY_COUNT ?? '0');
if (!Number.isInteger(hops) || hops < 0) {
throw new Error('Cannot derive client IP: TRUSTED_PROXY_COUNT is not a valid hop count');
}
const header = req.headers['x-forwarded-for'];
const parts = (Array.isArray(header) ? header.join(',') : (header ?? '')).split(',').map((part) => part.trim());
const candidate =
hops === 0 ? req.socket.remoteAddress : hops <= parts.length ? parts[parts.length - hops] : undefined;
if (!candidate || !isIP(candidate)) {
throw new Error('Cannot derive client IP: no valid IP for this TRUSTED_PROXY_COUNT');
}
return candidate;
};
app.post('/api/orders', async (req, res) => {
// In moments of risk, call a checkpoint within Composer to verify the request is allowed to proceed
const checkpointResponse = await composer.checkpoint({
checkpointName: 'PLACE_ORDER',
event: {
ip: getIp(req),
data: {
order: req.body.order
}
},
sourceToken: req.headers['x-dodgeball-source-token'], // Obtained from the JavaScript Client SDK, represents the device making the request
sessionId: req.session.id, // Assumes session middleware such as express-session
userId: req.session.userId,
useVerificationId: req.headers['x-dodgeball-verification-id'] // See Handling Additional Verification
});
if (composer.isAllowed(checkpointResponse)) {
// Proceed with placing the order
const placedOrder = await database.createOrder(req.body.order);
return res.status(200).json({
order: placedOrder
});
} else if (composer.isRunning(checkpointResponse)) {
// The checkpoint needs input from the user. See Handling Additional Verification
return res.status(202).json({
verification: checkpointResponse.verification
});
} else if (composer.isDenied(checkpointResponse)) {
// If the request is denied, you can return the verification to the frontend to display a reason message
return res.status(403).json({
verification: checkpointResponse.verification
});
} else {
// The checkpoint failed or finished without a decision. Decide how you would like to proceed: return the error, proceed, retry, or reject the request.
return res.status(500).json({
message: checkpointResponse.errors
});
}
});
app.listen(process.env.APP_PORT, () => {
console.log(`Listening on port ${process.env.APP_PORT}`);
});Checkpoint Parameters
| Parameter | Required | Description |
|---|---|---|
checkpointName | Yes | The name of the checkpoint to call. |
event.ip | Yes | The IP address of the device where the request originated. |
event.data | No | Data your checkpoint's workflow uses. For transactions, see Processing Spreedly Transactions. |
sourceToken | Either sourceToken or sessionId | The device token from the JavaScript Client SDK. |
sessionId | Either sourceToken or sessionId | The current session ID. |
userId | No | Your ID for the user, once you have one (for example, after registration). |
useVerificationId | No | The ID of a verification to resume. See Handling Additional Verification. |
options.timeout | No | How long, in milliseconds, the SDK keeps polling a verification that hasn't resolved. Defaults to 10 minutes. |
In Node.js, checkpoint() throws if a required parameter is missing. Network failures and timeouts don't throw: they come back as a failed response. getIp throws when it can't derive a valid IP, rather than substituting a guess.
Interpreting the Checkpoint Response
Every checkpoint call creates a verification. Use the helper methods to decide what to do, rather than reading status and outcome directly.
| Property | Description |
|---|---|
success | false if the request or the verification failed. |
errors | When success is false, a list of errors, each with a code and message. |
verification.id | The verification ID. Pass it to useVerificationId to resume this verification. |
verification.status | Where the verification is in processing. See Verification Statuses. |
verification.outcome | The decision. See Verification Outcomes. |
Verification Statuses
| Status | Description |
|---|---|
COMPLETE | The verification finished. |
PENDING | The verification is still processing. |
BLOCKED | The verification is waiting for input from the user. |
FAILED | The verification encountered an error and couldn't proceed. |
Verification Outcomes
| Outcome | Description |
|---|---|
APPROVED | The request should be allowed to proceed. |
DENIED | The request should be denied. |
PENDING | No decision has been reached yet. |
ERROR | The verification encountered an error and couldn't make a decision. |
Possible Responses and Helper Methods
Each Node.js helper takes the checkpoint response, for example composer.isAllowed(checkpointResponse).
| Response | status | outcome | Helper |
|---|---|---|---|
| Approved | COMPLETE | APPROVED | isAllowed() |
| Denied | COMPLETE | DENIED | isDenied() |
| Pending: still processing | PENDING | PENDING | isRunning() |
| Blocked: needs user input, such as MFA | BLOCKED | PENDING | isRunning() |
| Undecided: finished without a decision | COMPLETE | PENDING | isUndecided() |
| Error | FAILED | ERROR | hasError() |
| Timed out: polling failed three times in a row | isTimeout() |
When the polling time set by options.timeout runs out, the verification is still pending, not timed out: isRunning() returns true, and you can resume later with useVerificationId.
Handling Additional Verification
When a checkpoint needs input from the user, such as MFA or a 3DS challenge, isRunning returns true:
- Return the verification to your frontend.
- Pass it to the JavaScript Client SDK's
handleVerification(), which prompts the user. - When the user completes the step, your frontend retries the request with the verification ID, and your server passes it to
useVerificationId.
Each verification ID can only be used once, to prevent replay attacks.
Processing Spreedly Transactions
A checkpoint's workflow can run Spreedly transaction steps such as authorization, capture, and purchase. Transaction details go in event.data.
- Tokenize the payment method first. Collect and vault the payment method with a Spreedly tokenization method, then send the resulting payment method token to your server.
- Find the fields your checkpoint expects. In the checkpoint, open the Trigger node, then Expected Data > Copy Schema as JSON. The schema has the same shape as
event.dataand lists every field your workflow's steps need that no earlier step provides. - Send those fields in the checkpoint call:
const checkpointResponse = await composer.checkpoint({
checkpointName: 'PAYMENT',
event: {
ip: getIp(req),
data: {
// The exact fields depend on your workflow. Use your checkpoint's Expected Data schema.
paymentMethod: { token: req.body.paymentMethodToken },
transaction: { amount: 1000, currency: 'USD' } // amount is an integer in minor units: 1000 = $10.00
}
},
sourceToken: req.headers['x-dodgeball-source-token'],
sessionId: req.session.id
});Handle the response the same way as in Usage. A 3DS challenge follows Handling Additional Verification. The Client SDK doesn't take payment fields, and you don't need to add Spreedly.ThreeDS separately.
To test, set each transaction step's Gateway Token to a Spreedly test gateway. If a step's Gateway Token is blank, the step uses transaction.gatewayToken from event.data instead.
Updated 5 days ago

