CDR endpoints
Read the records your organization holds, and pay for the ones you want to download.
Authentication, the response envelope, and pagination are on
API conventions. All paths are relative to
https://api-next.expressconsent.com.
GET /v1/domains | Every domain you have records on |
GET /v1/domains/:domainId/cdrs | Page through the records on one domain |
GET /v1/cdrs/:cdrId | One record in full |
POST /v1/cdrs/:cdrId/collect | Pay for a record and gain download access |
GET /v1/domains
Lists the domains we have seen captures on for your organization, sorted alphabetically. There is no pagination; you get all of them. A domain appears as soon as a capture is uploaded from it, slightly before the first record on it becomes readable, so a newly added domain can list with no records yet.
curl -sS -H "X-API-Key: $EC_API_KEY" \
"https://api-next.expressconsent.com/v1/domains"{
"ok": true,
"data": { "domains": ["example.com", "other-example.com"] },
"requestId": "3f2504e0-4f89-11d3-9a0c-0305e82c3301"
}A domain identifier is the last two labels of the capture hostname, so captures from app.example.com
are filed under example.com.
Records shared into your account add their originating domain to this list.
GET /v1/domains/:domainId/cdrs
Pages through the records on one domain, newest first.
curl -sS -H "X-API-Key: $EC_API_KEY" \
"https://api-next.expressconsent.com/v1/domains/example.com/cdrs?pageSize=50"Query parameters
pageSize, pageToken, and order behave as described in
API conventions.
metadataKeystringoptionalReturn only records carrying this key in
customMetadataorautoMetadata. Must be sent together withmetadataValue; on its own it is a400 INVALID_ARGUMENT.metadataValuestringoptionalThe value to match. Matching is exact, but case-insensitive and whitespace-normalized. On its own, without
metadataKey, it is ignored.
This is how you find a record from a phone number or your own lead identifier eighteen months later.
Only one pair can be filtered at a time, and only values that were indexed are findable. See custom
on captureCDR() for what gets indexed.
A request with metadataKey ignores order and returns matches in an unspecified order. Sort them
yourself on createdAt if the order matters. Paging still works.
Response
{
"ok": true,
"data": {
"cdrs": [
{
"cdrId": "cdr_abc123",
"domainId": "example.com",
"domain": "example.com",
"createdAt": 1741234571230,
"contentType": "image/jpeg",
"size": 512000,
"collected": true
}
],
"nextPageToken": "cdr_abc123"
},
"requestId": "3f2504e0-4f89-11d3-9a0c-0305e82c3301"
}A list entry is a subset of the record shape: cdrId, domainId, domain,
createdAt, contentType, size, collected, downloadUrl, customMetadata, autoMetadata,
sessionId, packageId, webhookSuccessfullySentAt, pdfShareId, and pdfCreatedAt.
It does not carry organizationName, capturedAt, guest, producerOrgId, or any of the page-context
or consent fields; fetch a single record for those.
A page can contain fewer records than you asked for and still not be the last page, so follow
nextPageToken rather than counting.
GET /v1/cdrs/:cdrId
Returns one record with everything known about it. The record must belong to your organization, either because it was captured on your domain or because it was shared into your account.
curl -sS -H "X-API-Key: $EC_API_KEY" \
"https://api-next.expressconsent.com/v1/cdrs/cdr_abc123"{
"ok": true,
"data": {
"cdr": {
"cdrId": "cdr_abc123",
"domainId": "example.com",
"domain": "example.com",
"organizationName": "Example Leads",
"capturedAt": 1741234567890,
"createdAt": 1741234571230,
"contentType": "image/jpeg",
"size": 512000,
"collected": true,
"downloadUrl": "https://storage.googleapis.com/...",
"signerTelemetry": { "ip": "203.0.113.4", "ipChain": ["203.0.113.4"], "userAgent": "Mozilla/5.0 ..." },
"detectedDisclosureDetails": {
"userSubmitted": true,
"disclosures": [
{
"key": "tcpa",
"text": "By submitting this form you agree to be contacted at the number provided.",
"checkboxDetected": true,
"checkboxAffirmed": true,
"consentMechanism": "checkbox"
}
]
}
}
},
"requestId": "3f2504e0-4f89-11d3-9a0c-0305e82c3301"
}The record shape
Every field below is on the single-record response. List entries carry the subset named earlier.
Identity
cdrIdstringrequiredThe record identifier, the same value
captureCDR()returned.domainIdstringrequiredThe base domain the record is filed under, and the value to use in the list path.
domainstringrequiredThe domain recorded at capture.
organizationNamestringrequiredThe name of your organization, as a convenience for rendering. Falls back to
Organizationwhen it cannot be read.
Timestamps
Both are Unix timestamps in milliseconds.
capturedAtnumberrequiredWhen the browser took the capture. This is the evidentiary timestamp, the moment the person submitted. It comes from the visitor's device, so treat it as reported rather than authoritative.
createdAtnumberrequiredWhen we finished processing the record on our side. This is the field
ordersorts on and the one a record's age is measured from.Usually later than
capturedAt, but not guaranteed: on a record captured before we began recording client capture time the two are equal, and a visitor's clock running fast can putcapturedAtlater.
Evidence image
contentTypestringrequiredThe image's MIME type.
sizenumberrequiredThe image's size in bytes.
collectedbooleanrequiredWhether your organization has download access to this record. Two organizations asking about the same record legitimately get different answers. See access and collection.
downloadUrlstringoptionalA time-limited URL for the visual record, present only when
collectedistrue. Do not store it; store thecdrIdand request a fresh URL when you need one.Expires 300 seconds after the response is generated.
Page context
pageUrlstringoptionalThe URL of the captured page, including its query string. If you put personal data in query parameters, it is here.
clientTimezonestringoptionalThe visitor's IANA time zone, as reported by their browser.
timeOnPageMsnumberoptionalHow long the person had the page open before submitting. A useful signal for arguing a disclosure was readable rather than instantly dismissed.
signerTelemetryobjectoptionalWho submitted, as far as the network can say:
ipanduserAgent(either may benull),ipChainlisting every address in the forwarding chain, andgeowith an approximate location derived from the address. Best-effort: the whole object is absent when it could not be recorded.
Consent
detectedDisclosureDetailsobjectoptionalWhat we derived about consent at the detected moment of submission. 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, so test for the object rather than for this field.submitNodeIdidentifies the control that was pressed.derivedAtandderiverVersionrecord when the facts were derived and by which deriver, so a record can be re-derived from the stored capture and compared.disclosuresholds one entry per disclosure found, up to 50, each with:key: thedata-ec-disclosurevalue, or a generated onetext: the disclosure language read from the page, whitespace-collapsed and truncated at 5,000 characterscheckboxDetected: whether a consent checkbox was bound to it and readablecheckboxAffirmed: whether every readable checkbox bound to it was checked; present only whencheckboxDetectedistruecheckboxNodeId: the first bound checkbox element, for auditconsentMechanism:checkbox,button_submission, ornone_detected
Read
consentMechanismin preference to the booleans. It is absent only on records derived before the field existed.disclosuresarrayoptionalDeprecated. The booleans an integrator asserted through the
disclosuresoption, rather than anything we observed. Present only when that option was passed.consentLanguagearrayoptionalDeprecated. Disclosure text captured by SDK versions predating derived detection.
Your metadata
customMetadataobjectoptionalExactly what you passed as
customon the capture call.autoMetadataobjectoptionalFields we derived from the captured page, never anything you supplied. Present only when something was found, and filterable exactly like
customMetadata.phones,firstNames, andlastNamesare sorted arrays read from identified form fields, withphonesalso picking up numbers found in page text and normalized to+1form rather than stored as written.pageTitleandformActioncome from the page itself.utmSource,utmMedium,utmCampaign,utmTerm,utmContent,gclid,fbclid, andmsclkidcome from the page URL's query string.Keys up to 100 characters, 4,000 characters serialized in total. At most 10 phones and 5 of each name. pageTitle is cut at 200 characters, formAction at 500, and values from the URL at 200.
Grouping and provenance
sessionIdstringoptionalThe session this capture belonged to. See sessions and Package CDRs.
packageIdstringoptionalThe Package CDR this record belongs to, when it belongs to one.
Present only when the package groups more than one record. Every capture is filed into a session-derived package, so a package holding a single record tells you nothing and is omitted. Its presence means this record is part of a multi-step or co-registration flow whose records only tell the full story together. See sessions and Package CDRs.
guestbooleanoptionaltruewhen this record reached your account by being shared with you rather than captured on your own site.producerOrgIdstringoptionalThe organization that originally captured the record. Present on shared records, and unchanged by further re-sharing, so it identifies the source rather than whoever passed it to you.
Delivery status
webhookSuccessfullySentAtnumberoptionalWhen your webhook acknowledged this record. Absent means it has not been delivered, either not yet or not successfully.
pdfShareId and pdfCreatedAt are in the response type but nothing populates them: a PDF evidence
report is generated against a dashboard share link and recorded there, not on the record. Do not
branch on either field.
POST /v1/cdrs/:cdrId/collect
Pays for a record and grants your organization download access to it. This is a billable call. It takes no request body.
curl -sS -X POST -H "X-API-Key: $EC_API_KEY" \
"https://api-next.expressconsent.com/v1/cdrs/cdr_abc123/collect"{
"ok": true,
"data": { "cdrId": "cdr_abc123", "collected": true, "alreadyCollected": false },
"requestId": "3f2504e0-4f89-11d3-9a0c-0305e82c3301"
}Calling it again for the same record is safe and does not charge you twice. Most organizations never call it: an organization with billing set up and auto-collect left on (the default) has captures on its own domains collected automatically, and receiving a shared record collects it in the same request. It exists for the case where you deliberately did not pay earlier and now want the evidence. See access and collection.
collectedbooleanrequiredAlways
true. The call either succeeds or returns an error.alreadyCollectedbooleanrequiredWhether your organization could already download this record before the call.
truemeans the call changed nothing and you were not charged again.It answers only that question. It does not tell you how you got access: paying for it earlier and receiving it free from an organization that had already paid both report
true.
Organizations without billing set up cannot collect. Such an organization normally has no API access
either, so in practice every request from it returns 403 API_ACCESS_DISABLED and never reaches this
endpoint; if API access was granted to it explicitly, the call returns 403 PASSTHROUGH_ORG_CANNOT_COLLECT instead. That is the passthrough organization state, and changing it
means talking to us.
Errors
Every code, its HTTP status, and what to do about it is on API errors. The ones specific to these endpoints:
| Status | Code | Cause |
|---|---|---|
| 400 | INVALID_ARGUMENT | metadataKey without metadataValue, a metadataKey and metadataValue too long to index, or a pageToken that is not a record on this domain |
| 403 | PASSTHROUGH_ORG_CANNOT_COLLECT | Your organization has no billing set up |
| 404 | NOT_FOUND | No such record for your organization, or a record that has not finished rendering. On collect, also an organization record we could not read, so a 404 there is not always about the identifier in the path. |
| 410 | CDR_INVALID | The record was flagged invalid and cannot be read, collected, or shared |
A record flagged invalid is also dropped from list results rather than returned with a flag, so a
record that vanishes from a listing and answers 410 on a direct read has been invalidated. The
response type carries invalidated, invalidatedAt, and invalidatedReason, but no path through the
API returns them: an invalid record is filtered from lists and refused on a direct read, so those three
fields are unreachable.