Client SDK
About the Client SDK
The Composer Client SDK for JavaScript runs in your users' browsers. It isn't strictly required, but it's highly recommended:
- It augments your data. The SDK identifies the device and adds context such as device fingerprinting, behavior intelligence, and session intelligence. This improves risk decisions and can be used to produce session replays for fraud investigations.
- It runs third-party frontend code for you. Many fraud and verification services ship their own frontend code, such as Veriff's JavaScript SDK for ID verification. The Client SDK loads and runs it, so you don't manage it yourself.
- It handles checkpoint outcomes in real time. When a workflow needs more from the user, such as MFA, the SDK shows that step, and callbacks let you update your UI with the result.
Prerequisites
You will need to obtain a public API key for your application from the Dodgeball developer center. Use the public key in the browser, and never put your secret key in frontend code. For setup instructions, see the Client SDK README. If you don't already have an account, contact support.
The SDK supports Chrome 85, Edge 85, Firefox 79, Safari 14 or later. Internet Explorer is not supported.
Related
The Client SDK works alongside the Server SDKs, which call checkpoints from your backend.
For a runnable reference app, see composer-examples. Its client/web-next app uses the Client SDK and calls the example servers.
Installation
Use npm to install the SDK:
npm install @spreedly/composer-sdk-clientAlternatively, using yarn:
yarn add @spreedly/composer-sdk-clientLoading with a script tag
The SDK is also available from unpkg. Pin the version so a new release can't change your page without you knowing, and copy the integrity value for that version from its release notes so the browser won't run the file if it's ever changed. crossorigin="anonymous" is required for the browser to check it:
<script src="https://unpkg.com/@spreedly/[email protected]/dist/umd/index.js"
integrity="sha384-..." crossorigin="anonymous"></script>
<script>
const composer = new Composer('public-api-key...');
</script>The script sets window.Composer. It also sets window.SpreedlyComposer, which holds every export (SpreedlyComposer.Composer, SpreedlyComposer.LogLevel and so on), in case another script on your page already uses the name Composer.
Content Security Policy
The SDK adds RequireJS and your workflow's integration code to the page as inline scripts. If your page sets a Content-Security-Policy, its script-src must allow them with either:
- a nonce on the script that loads the SDK, plus
'strict-dynamic', or 'unsafe-inline', plus any host your integrations load scripts from.
A script-src without 'strict-dynamic' or 'unsafe-inline' blocks them, as does Trusted Types enforcement (require-trusted-types-for 'script'), which the SDK doesn't support. In either case the SDK logs an error and finishes starting up without integrations.
connect-src must also allow your apiUrl (https://api.dodgeballhq.com by default).
Configuration
Initialize the SDK once, when your application loads, with your public API key.
import { Composer } from '@spreedly/composer-sdk-client';
const composer = new Composer('public-api-key...', {
apiUrl: 'https://api.sandbox.dodgeballhq.com',
logLevel: 'ERROR'
});In React, use the useComposer hook. It creates the instance the first time it's called with your public API key, and returns the same instance everywhere after that:
import { useComposer } from '@spreedly/composer-sdk-client';
import { useEffect } from 'react';
function App({ session, user }) {
const composer = useComposer('public-api-key...'); // Once, in your root component
useEffect(() => {
composer.track(session?.id, user?.id);
}, [session?.id, user?.id]);
// ...
}
function Checkout() {
const composer = useComposer(); // Once initialized, you can omit the public API key
// ...
}Every option is optional.
| 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. |
apiVersion | v1 | The Composer API version to use. |
logLevel | INFO | TRACE, INFO, ERROR, or NONE. TRACE and INFO are useful for debugging, ERROR only logs errors, and NONE turns off logging. |
disableCookies | false | When true, the SDK keeps the source token in localStorage instead of a cookie. |
isEnabled | true | When false, the SDK makes no API calls: getSourceToken() returns a placeholder token and handleVerification() calls onApproved. Useful for local development. |
integrationTimeout | 2000 | How long each third-party integration may take to load, in milliseconds, before the SDK skips it. |
sessionId | The session ID, if you have it when you create the instance. You can also pass it to track(). | |
userId | The user ID, if you have it when you create the instance. Requires sessionId. |
The constructor throws ComposerMissingConfigError when the public API key is missing, and ComposerInvalidConfigError when apiVersion or logLevel isn't one of the values above.
Usage
Every application has key moments of risk where it needs to check that a request should be allowed to proceed, such as registration, login, placing an order, creating a listing, leaving a review, adding a payment method, changing bank account information, redeeming a coupon, and transferring funds. In Composer, these moments are called checkpoints. Your API calls them with a Server SDK and returns the result to your frontend.
Once you know the session, and later the user, pass them to track(). At a moment of risk, such as placing an order, get a source token and send it to your API. Your API calls a checkpoint and returns the verification. Pass the verification to handleVerification(), which runs any extra steps the workflow needs and calls you back with the result:
// As soon as you have a session ID
composer.track(session.id);
// When the user logs in
composer.track(session.id, user.id);
const placeOrder = async (order, previousVerificationId = null) => {
const sourceToken = await composer.getSourceToken();
const response = await fetch('/api/orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-dodgeball-source-token': sourceToken, // Pass the source token to your API
...(previousVerificationId && { 'x-dodgeball-verification-id': previousVerificationId })
},
body: JSON.stringify({ order })
});
const body = await response.json();
if (response.status === 200) {
showOrderPlaced();
return;
}
if (!body.verification) {
showError('Something went wrong. Please try again.');
return;
}
composer.handleVerification(body.verification, {
onVerified: async (verification) => {
// An extra step was completed and the request is approved. Retry with the verification ID
await placeOrder(order, verification.id);
},
onApproved: async () => {
// Approved with no extra step
showOrderPlaced();
},
onDenied: async (verification) => {
showOrderDenied();
},
onError: async (error) => {
showError(error.details);
}
});
};This matches the API in the Server SDKs example: it returns 200 when the order is placed, 202 with the verification when the workflow needs more from the user, and 403 with the verification when the order is denied. Your API can receive the source token and verification ID however you like; the headers above are a convention.
Track Parameters
| Parameter | Required | Description |
|---|---|---|
sessionId | Yes | The ID of the current session. Any unique string representing the user's activity in your system. If you don't have one, use the source token from getSourceToken(). |
userId | No | The ID of the current user. Send it as soon as the user is authenticated. |
Source Tokens
getSourceToken() returns a token representing this device's recent activity. Source tokens expire after 1 hour, and the SDK refreshes them automatically. Call getSourceToken() immediately before each request rather than storing the token yourself: Composer rejects requests made with an expired source token.
refreshSourceToken() replaces the current source token with a new one, for example when a user logs out.
Both return a promise, or take a callback that's called with the token:
const sourceToken = await composer.getSourceToken();
composer.refreshSourceToken((newSourceToken) => {});Verification Callbacks
| Callback | Required | Called when |
|---|---|---|
onVerified(verification) | Yes | The user completed an extra step and the request is approved, but your API hasn't acted yet. Call your API again with verification.id, which it passes to useVerificationId. |
onApproved(verification) | Yes | The request was approved without an extra step, so your API already acted on it. |
onDenied(verification) | No | The request was denied. |
onPending(verification) | No | The verification is still processing. |
onBlocked(verification) | No | The verification is waiting on the user, such as an MFA challenge. |
onUndecided(verification) | No | The workflow finished without a decision. Your application decides how to proceed. |
onError(error) | No | The verification didn't finish, and error.details has more information. error.errorType is CANCELLED if the user closed the extra step, or SYSTEM if the verification failed on the server (for example, a third-party service was unavailable), in which case offering the user a retry can help. |
handleVerification() takes an optional third argument, { maxDuration }: how long, in milliseconds, the SDK keeps polling a verification that hasn't finished. Defaults to 24 hours. When it runs out, polling stops without calling another callback, so the last one you received was onPending or onBlocked.
You won't need to read a verification's status and outcome yourself; the callbacks cover each case. For what they mean, see Interpreting the Checkpoint Response.
Handling Additional Verification
Some workflows need more from the user before they decide, such as a 3DS challenge, or 2FA at login only on a new device or from an IP address far from the user’s last one. When that happens:
- Your API returns the verification without acting on the request.
handleVerification()shows the extra step to the user.- When the user completes it and the request is approved,
handleVerification()callsonVerified. - Your frontend retries the request with
verification.id, and your API passes it touseVerificationId.
Each verification ID can only be used once, to prevent replay attacks.
Changes from the Dodgeball client SDK
If you're upgrading from @dodgeball/trust-sdk-client, install @spreedly/composer-sdk-client and update your imports. The old names still work, but are deprecated:
| Old name | New name |
|---|---|
Dodgeball | Composer |
useDodgeball | useComposer |
DodgeballApiVersion | ComposerApiVersion |
IDodgeballConfig | IComposerConfig |
window.Dodgeball | window.Composer |
The CHANGELOG lists everything that changed. Read its Changed, Deprecated and Removed sections for the steps to migrate, including the new script-tag URL and the session cookie now requiring HTTPS.
Updated 5 days ago

