Skip to main content

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.

FieldDescription
idUnique ID for this acceptance record
receiptIdSave this to your database to cross-reference evidence records during audits
timestampWhen the acceptance occurred
deploymentIdWhich deployment was accepted
templateIdWhich template was shown
versionTemplate version the user saw
userIdThe user ID passed at SDK initialization, if provided
userEmailThe user email passed at SDK initialization, if provided
snapshotHashHash of the agreement snapshot — confirms what was displayed
Save the receipt ID

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.

Coming Soon

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:

ErrorCauseFix
"Container not found"containerId element missing from DOMEnsure the element exists before calling render()
401 UnauthorizedInvalid or missing API keyCheck Click → Settings → API Keys
"Invalid deployment ID"Deployment not found or inactiveConfirm the deployment is active in the Click dashboard
"Fetch failed"Network issueSDK retries automatically; check connectivity
Coming Soon

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 receiptId to your own database for audit cross-referencing
  • Blocks account creation on decline — never silently ignores it
  • Wraps everything in try/catch so 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