Server SDKs
About the Server SDKs
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.
Every server SDK has the same methods and behavior, with names that follow each language's conventions. The examples on this page use Node.js. Your SDK's README has the same examples in your language. The few places where the SDKs behave differently are listed in Differences between SDKs.
Prerequisites
You will need to obtain a secret API key for your application from the Dodgeball developer center. For setup instructions, see your SDK’s README in Available SDKs. 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). If you're upgrading from a Dodgeball server SDK, also see Changes from the Dodgeball server SDKs.
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 | composer-sdk-server-node |
| Python | spreedly-composer-sdk-server | composer-sdk-server-python |
| Ruby | spreedly-composer-sdk-server | composer-sdk-server-ruby |
| Go | github.com/spreedly/composer-sdk-server-go | composer-sdk-server-go |
| PHP | spreedly/composer-sdk-server | composer-sdk-server-php |
| .NET | Coming soon | composer-sdk-server-dotnet |
| Java | Coming soon | 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. |
Every SDK has these options with the same defaults, named to follow each language’s conventions. See your SDK’s README for the exact names.
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 | No | The IP address of the device where the request originated. Send it whenever an end user is present. Leave it out for checkpoints with no user, such as subscription renewals. |
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. The SDK always polls at least once, even if this is very small. |
checkpoint() throws if a required parameter is missing (Go returns the error instead). Network failures and timeouts never throw: they come back as a failed response, with success false and an error code of 500. 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 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.
Differences between SDKs
The SDKs behave the same except for the following:
- Missing required parameters in Go:
Checkpointreturns aComposerMissingParameterErroras its error value instead of throwing. The other SDKs throw. - Request timeout in Python and Ruby: the request timeout applies to each stage of a request (connecting, then waiting for data), not to the whole request, because their HTTP libraries have no single overall deadline. A slow response can take longer than
request_timeout_msin total. The other SDKs cap the whole request. - Empty objects in PHP: the SDK decodes responses as PHP arrays, so an empty object in
verification->stepDataarrives as[]. When you send data, use astdClass(or(object) []) for an object, becausejson_encodesends an empty array, or one keyed0, 1, 2…, as a JSON list.
Changes from the Dodgeball server SDKs
If you're upgrading from a Dodgeball server SDK, your SDK's changelog lists everything that changed, including renamed packages and classes and new default behavior. Read its Changed and Removed sections for the steps to migrate.
| Language | Upgrading from | Changelog |
|---|---|---|
| Node.js | @dodgeball/trust-sdk-server | CHANGELOG.md |
| Python | dodgeball-server-sdk | CHANGELOG.md |
| Ruby | dodgeball-trust-sdk-ruby | CHANGELOG.md |
| Go | github.com/dodgeballhq/dodgeball-trust-sdk-go | CHANGELOG.md |
| PHP | dodgeball/dodgeball-sdk-server | CHANGELOG.md |
| .NET | Dodgeball.TrustServer | CHANGELOG.md |
| Java | com.dodgeballhq.protect:protect-core | CHANGELOG.md |
Updated about 1 hour ago

