The cdr.completed event
The only event we send. One per completed record, POSTed as JSON to your webhook URL.
Setup, retries, and delivery guarantees are on set up webhooks.
{
"event": "cdr.completed",
"orgId": "org_abc123",
"cdrId": "cdr_abc123",
"domainId": "example.com",
"domain": "example.com",
"capturedAt": 1741234567890,
"createdAt": 1741234571230,
"ip": "203.0.113.4",
"userAgent": "Mozilla/5.0 ...",
"downloadUrl": "https://storage.googleapis.com/...",
"customMetadata": { "phone": "555-0100", "leadId": "L-8823" },
"sessionId": "sess_9f2a",
"detectedDisclosureDetails": {
"userSubmitted": true,
"submitNodeId": 214,
"disclosures": [
{
"key": "tcpa",
"text": "By submitting this form you agree to be contacted at the number provided.",
"checkboxDetected": true,
"checkboxAffirmed": true,
"consentMechanism": "checkbox"
}
],
"derivedAt": 1741234571180,
"deriverVersion": "disclosure-details-deriver@3"
},
"webhookId": "6f9619ff-8b86-4fd1-b42f-7fd68a2c0f5a",
"timestamp": 1741234571500
}Optional fields are omitted rather than sent as null, except where a row below says otherwise. Do not assume a field is present because it was present last time.
Event and delivery
event"cdr.completed"requiredThe event type. This is the only value there is; branch on it anyway, so adding a second event does not break your handler.
webhookIdstringrequiredA unique identifier for this delivery attempt, matching the
X-EC-Webhook-Idheader.It changes on every retry, so it is not an idempotency key. Deduplicate on
cdrId.timestampnumberrequiredWhen this delivery attempt was assembled.
Unix milliseconds. The X-EC-Timestamp header carries the same instant in seconds, and it is the seconds value the signature is computed over.
Identity
cdrIdstringrequiredThe record identifier, the same value
captureCDR()returned in the browser. This is the field to join on and the field to deduplicate on.orgIdstringrequiredYour organization identifier, the same value you put in
data-ec-cid. Useful when several organizations you operate point at one endpoint.domainIdstringrequiredThe base domain the record is filed under, and the value the API's list path takes.
domainstringrequiredThe domain recorded at capture.
Timestamps
Both are Unix timestamps in milliseconds.
capturedAtnumberrequiredWhen the browser took the capture, the evidentiary moment. It comes from the visitor's device, so treat it as reported rather than authoritative.
createdAtnumberrequiredWhen we finished processing the record. Sort on this one if you need an order; deliveries arrive in no particular order.
Usually later than
capturedAt, but not guaranteed: the two are equal on a record captured before we began recording client capture time, and a visitor's fast clock can invert them.
Evidence
downloadUrlstringoptionalA time-limited URL for the visual record.
Present when your organization can already download the record and the record has not been invalidated. See access and collection.
It can also be absent for neither of those reasons: minting the URL is best-effort, and a failure there sends the event without it rather than retrying. So absent never means the image is missing; ask the API for a fresh URL by
cdrId.Seven days is a working window, not storage. Store the
cdrIdand ask the API for a fresh URL.Expires seven days after the delivery attempt was assembled, not after you received it. A URL fetched from the API lasts 300 seconds instead.invalidatedbooleanoptionalPresent, and
true, only when we have flagged this record invalid. It is absent on a valid record rather thanfalse.The event is still delivered so you learn the record exists and must not be used or sold.
downloadUrlis withheld.
Who submitted
ipstring | nulloptionalThe consumer's IP address.
nullwhen it could not be determined.ipanduserAgentcome from one telemetry record, so they are present together or absent together. Both are absent when no telemetry was recorded for the capture, or when we could not read it.userAgentstring | nulloptionalThe consumer's user agent string, present and absent under exactly the same conditions as
ip.
These two fields are flat, and they are all there is. The forwarding chain and
the approximate location are on the API's signerTelemetry object and are not
sent here. Fetch the record if you need them.
Consent
detectedDisclosureDetailsobjectoptionalWhat we derived about consent at the detected moment of submission, read back out of the captured page rather than reported by your code. Absent entirely when no submission was detected, which is the strongest single signal that a record is weak evidence.
userSubmittedis alwaystrue; the object's presence is the signal that a submission was observed.submitNodeIdidentifies the control that was pressed, when we could pin its node.derivedAtandderiverVersionrecord when the facts were derived and by what, so they can be re-derived from the stored capture and compared.disclosuresholds one entry per disclosure found, up to 50.key,text,checkboxDetected, andconsentMechanismare always on an entry;consentMechanismis one ofcheckbox,button_submission, ornone_detected, and is worth reading in preference to the booleans.checkboxAffirmedandcheckboxNodeIdappear together and only when a readable checkbox bound to that disclosure, andcheckboxAffirmedistrueonly when every readable checkbox bound to it was checked.The array can be empty. A page with a detected submission and no readable disclosure yields the object with
disclosures: [], which is what an untagged or legacy-tagged page produces. Testdisclosures.lengthrather than the presence of the object. Full semantics are on CDR endpoints.disclosuresarrayoptionalDeprecated. The
{ key, language, agreed }entries produced by thedisclosurescapture option: booleans your code asserted, not anything we observed. Present only when a page still passes that option. See migrate to derived consent.consentLanguagearrayoptionalDeprecated. Disclosure text detected in the browser by pages that have not adopted
data-ec-disclosure. Present only for those pages.
Your metadata
customMetadataobjectoptionalExactly what you passed as
customon the capture call. This is where your own lead identifier comes back to you, and the reason to pass one.autoMetadataobjectoptionalFields we derived from the captured page, never anything you supplied:
phones,firstNames,lastNames,pageTitle,formAction, and the UTM and click parameters from the page URL. Present only when something was found. Shapes and caps are on CDR endpoints.
Grouping
sessionIdstringoptionalThe session this capture belonged to. See sessions and Package CDRs.
packageIdstringoptionalThe package this record was filed into.
Unlike the API's field of the same name, this one is not a signal that the record is part of a multi-step flow. When it is present it names the record's session-derived package whether that package holds one record or ten. It is absent when the capture arrived carrying no package data, which is unusual but not something to assume away. The record is still filed into a package; this field has no name to report for it.
To find out whether a record has siblings, read
packageIdfrom the API instead, where it is present only for a package holding more than one record.