captureCDR()
Capture a snapshot of the page, upload it, and get back a cdrId confirming the evidence is stored.
Usage
Call captureCDR() from your form’s submit handler and await it before the page navigates. Awaiting keeps the page alive during capture, confirms the evidence was stored, and returns the cdrId and shareUrl.
form.addEventListener("submit", async (event) => {
event.preventDefault();
// Read form values synchronously BEFORE the first await.
const phone = form.phone.value;
try {
// captureCDR() must be the first await — calling anything async before it
// breaks the link to the user-triggered submission.
const { cdrId, shareUrl } = await window.ExpressConsent.captureCDR({
autoShare: true,
custom: { phoneNumber: phone },
});
// Pass shareUrl to the lead buyer so they can access the evidence.
await saveLead({ phone, cdrId, shareUrl });
} catch (err) {
// Log it and let the submit proceed — never block submission on a capture failure.
console.error("Capture failed:", err);
}
form.submit();
});Do not await anything before captureCDR(). The SDK ties the capture to the user-triggered submit event to verify the capture is legitimate. Awaiting anything else first (a fetch, a validation call, a delay) breaks that link. Reading synchronous values from the form before the call is fine.
Separately, if the page navigates or the form unmounts before the promise resolves, the CDR may not be saved. The pattern is always: preventDefault(), await captureCDR(), then submit.
Verifiable consent: tag your page
Tag the consent parts of your page and each CDR captures exactly what the user did: which disclosure was shown, whether the consent checkbox was checked, and the button they submitted with. This makes the evidence stronger, and it lets a lead buyer confirm a lead meets their requirements, such as a disclosure that names their company.
The tags
data-ec-disclosure="<id>": the disclosure language the user is agreeing to. The text is captured so the record is searchable. Theidis optional when the page has a single disclosure.data-ec-submit: the button the user submits with. ExpressConsent outlines this exact control in the rendered evidence, so the record shows precisely how the user submitted.data-ec-consent-checkbox: a consent checkbox. ExpressConsent records whether it was checked when the user submitted. Put it on a native<input type="checkbox">, on a custom widget withrole="checkbox"orrole="switch", or on a container that wraps exactly one such control.data-ec-consent-for="<id>": which disclosure a checkbox governs. Optional when the page has exactly one disclosure and one checkbox; required when there is more than one.
<!-- The disclosure language the user is agreeing to. -->
<p data-ec-disclosure="tcpa">
By submitting, you agree to be contacted at the number provided,
including by automated technology.
</p>
<!-- A consent checkbox bound to that disclosure (optional). -->
<input type="checkbox" data-ec-consent-checkbox data-ec-consent-for="tcpa" />
<!-- The button the user submits with. -->
<button type="submit" data-ec-submit>Submit</button>Call captureCDR() when the user activates your tagged data-ec-submit button. ExpressConsent records the submission and outlines that button in the evidence.
If you want to gate the capture on whether the checkbox is checked, you can read its state synchronously before calling captureCDR():
const consentChecked = document.querySelector("[data-ec-consent-checkbox]").checked;
if (consentChecked) {
// captureCDR() still must be the first await.
await window.ExpressConsent.captureCDR();
}That is entirely your call. ExpressConsent derives the checkbox state independently server-side regardless; this just lets you decide whether to capture at all.
Multiple disclosures
When there is more than one disclosure or checkbox, bind each checkbox to its disclosure with data-ec-consent-for.
<!-- With more than one disclosure, bind each checkbox explicitly. -->
<p data-ec-disclosure="sms">By checking this box, you agree to receive SMS messages...</p>
<input type="checkbox" data-ec-consent-checkbox data-ec-consent-for="sms" />
<p data-ec-disclosure="email">By checking this box, you agree to receive marketing email...</p>
<input type="checkbox" data-ec-consent-checkbox data-ec-consent-for="email" />
<button type="submit" data-ec-submit>Submit</button>What ExpressConsent reports: the consent mechanism
For each disclosure, ExpressConsent derives a consentMechanism at the moment of the detected submission. It is one of three values, so a lead buyer can filter on exactly how consent was expressed:
checkbox: a consent checkbox bound to the disclosure was checked (affirmative opt-in).button_submission: the disclosure had no consent checkbox, so consent was expressed by submitting the form (the disclosure was shown and the user submitted with an affirmative control).none_detected: no affirmative consent was detected. A bound checkbox was unchecked or unreadable, a consent checkbox on the page did not bind to a disclosure, or the user submitted with a negative control (e.g. a “No” / “Decline” button) on a disclosure that had no checkbox.
You never declare the mechanism. ExpressConsent reads it from the captured evidence and surfaces it as consentMechanism on each disclosure in the API and webhook.
Masking
Masking redacts sensitive data (SSN, CVV, passwords) from the evidence before it leaves the browser. Masked data is gone permanently; you cannot recover it, even as the org owner.
The more you mask, the less useful the visual snapshot is as evidence. Only mask what is strictly necessary, such as payment details or government IDs. Never mask consent language, checkboxes, or submit buttons.
Mask a single input
<label>
SSN
<input name="ssn" data-expressconsent-mask />
</label>Mask an entire section
The attribute is inherited by descendants. Place it on a container to redact everything inside.
<section data-expressconsent-mask>
<h3>Payment details</h3>
<input name="cardNumber" />
<input name="cvv" />
<!-- Everything inside is redacted. -->
</section>Inputs
All options are optional. Call captureCDR() with no arguments and you still get a valid CDR.
custom
Arbitrary metadata stored with the CDR. Every key becomes a queryable field: you can filter CDRs by any single key/value pair using the API’s metadataKey/metadataValue params. Include anything you might want to look a CDR up by later: phone numbers, email addresses, campaign IDs, form names. Must be JSON-serializable. Max 16 KB.
autoShare
Generate a share URL for a lead buyer during the upload, with no separate API call. Pass true for the default 30-day expiry, or an options object for a custom one:
autoShare: true: 30-day expiry (default)autoShare: { expiresInMs: 604800000 }: custom expiry (max 2 years)
const { cdrId, shareUrl } = await window.ExpressConsent.captureCDR({
autoShare: true,
});When enabled, the result includes shareUrl, shareToken, and shareExpiresAt. See Sharing with lead buyers.
timeoutMs
Optional request timeout in milliseconds. Overrides the SDK’s default adaptive timeout budget.
Return values
Always present:
cdrId: the evidence ID. Store this with your lead record.packageData: session grouping info, containingpackageId(the session-level package). Relevant only if you build package-aware workflows.
Present when autoShare is enabled:
shareUrl: the full absolute URL the lead buyer opens (with their API key) to receive the evidence.shareToken: the raw share token (the last segment of the URL).shareExpiresAt: token expiry as a Unix timestamp in milliseconds.
Error behavior
captureCDR() throws when it cannot guarantee the evidence was persisted:
- SDK not loaded (missing script tag or
data-ec-cid) - Network failure or timeout
CAPTURE_DISABLED: capture has been administratively disabled for this organization. Contact ExpressConsent support if you believe this is an error.
Wrap the call in a try/catch, log the error, and let the form submit proceed. Do not block submission on a capture failure.