Events & Callbacks
When a user accepts, declines, or lets a session time out, the SDK fires a callback so your application can respond — unlocking a feature, redirecting to the next step, or surfacing an error message.
Callbacks
Pass callbacks into sdk.render(). onAccept is almost always needed; the others are optional. Your deployment ID is found on the deployment detail page in the dashboard — see the Quickstart for where to find it.
await sdk.render('YOUR_DEPLOYMENT_ID', {
onAccept: (acceptance) => {
// User accepted — proceed with your app flow
console.log('Accepted:', acceptance.receiptId);
},
onDecline: () => {
// User declined — block access or surface a message
// Only fires if your template is configured to show a decline button
},
onClose: () => {
// The agreement widget was closed without a response
// Clean up your UI if needed
},
});
What onAccept gives you
The onAccept callback receives an acceptance object with everything you need to record and reference the acceptance in your own system.
| Field | Description |
|---|---|
id | Unique ID for this acceptance record |
receiptId | Save this to your database to cross-reference evidence records during audits |
timestamp | When the acceptance occurred |
deploymentId | Which deployment was accepted |
templateId | Which template was shown |
version | Template version the user saw |
userId | The user ID passed at SDK initialization, if provided |
userEmail | The user email passed at SDK initialization, if provided |
snapshotHash | Hash of the agreement snapshot — confirms what was displayed |
Store acceptance.receiptId in your own database. It lets you match your internal records to Propper's evidence bundles if you're ever asked to verify an acceptance in an audit.
Screenshot: click-sdk-acceptance-callback, browser console showing an acceptance object with receiptId and timestamp fields
onDecline
Only fires if your template is configured to show a decline button — set in the Content Settings tab when building the template. If you're gating access behind acceptance, block your app flow inside this callback — don't silently ignore it.
onClose
Fires when the agreement widget is closed without a response (for example, the user dismisses a modal without accepting or declining). Use it to clean up your UI:
onClose: () => {
document.getElementById('terms-container').remove();
}
Error handling
Wrap sdk.render() in a try/catch to handle network failures and configuration problems gracefully:
try {
await sdk.render('YOUR_DEPLOYMENT_ID', {
onAccept: (acceptance) => { /* ... */ },
});
} catch (error) {
console.error('SDK error:', error.message);
}
Common errors:
| Error | Cause | Fix |
|---|---|---|
"Container not found" | containerId element missing from DOM | Ensure the element exists before calling render() |
401 Unauthorized | Invalid or missing API key | Check Click → Settings → API Keys |
"Invalid deployment ID" | Deployment not found or inactive | Confirm the deployment is active in the Click dashboard |
"Fetch failed" | Network issue | SDK retries automatically; check connectivity |
Screenshot: click-sdk-error-console, browser console showing a 401 Unauthorized SDK error with the message and relevant fields highlighted
Checking acceptance without rendering
To check whether a user has already accepted before deciding whether to render:
if (sdk.hasAccepted('YOUR_DEPLOYMENT_ID')) {
proceedToNextStep(); // Already accepted, skip the form
} else {
await sdk.render('YOUR_DEPLOYMENT_ID', { onAccept: /* ... */ });
}
Server-side notifications with webhooks
SDK callbacks are ideal for in-page reactions. For reliable backend recording — writing acceptances to a database, triggering downstream workflows, or catching events the frontend might miss — use webhooks instead.
Webhooks deliver a server-side notification for every acceptance, decline, expiry, and evidence creation event. They're configured in Click Settings → Webhooks, which also covers available events, payload format, and signature verification.
Complete example: sign-up flow with Terms of Service
A production-ready pattern combining all the above — cache check, acceptance recording, decline handling, and error recovery:
import { init } from '@propper/click-sdk';
// Initialize once on app load, not inside the handler
const sdk = init({
apiKey: process.env.REACT_APP_CLICK_API_KEY,
environment: 'production',
userId: currentUser?.id,
userEmail: currentUser?.email,
});
async function handleSignUp(formData) {
try {
// Skip rendering if the user already accepted
if (sdk.hasAccepted('YOUR_DEPLOYMENT_ID')) {
await createAccount(formData);
return;
}
await sdk.render('YOUR_DEPLOYMENT_ID', {
containerId: 'tos-container',
onAccept: async (acceptance) => {
// Save the receipt ID for audit cross-referencing
await saveToDatabase({
userId: currentUser.id,
receiptId: acceptance.receiptId,
acceptedAt: acceptance.timestamp,
});
await createAccount(formData);
},
onDecline: () => {
setFormError('You must accept the Terms of Service to continue.');
},
});
} catch (error) {
setFormError('Failed to load terms. Please refresh and try again.');
}
}
What this pattern does:
- Initializes the SDK once at app load, not inside the handler
- Checks the cache before rendering to avoid re-prompting returning users
- Saves
receiptIdto your own database for audit cross-referencing - Blocks account creation on decline — never silently ignores it
- Wraps everything in
try/catchso SDK errors don't crash the sign-up form
Troubleshooting
onAccept never fires.
First, confirm the callback is passed inside the options object to sdk.render() — assigning it as a property on the SDK instance won't work. Second, onAccept only fires when the user explicitly clicks the acceptance button. It does not fire if the modal is dismissed or the page is navigated away.
onDecline never fires.
The decline option is only shown if your template is configured to allow it. Check Content Settings in your template and confirm the decline button is enabled. See Content Settings for details.
Next: Customization →