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/domainsEvery domain you have records on
GET /v1/domains/:domainId/cdrsPage through the records on one domain
GET /v1/cdrs/:cdrIdOne record in full
POST /v1/cdrs/:cdrId/collectPay 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.

bash
curl -sS -H "X-API-Key: $EC_API_KEY" \
  "https://api-next.expressconsent.com/v1/domains"
json
{
  "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.

bash
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.

metadataKeystringoptional

Return only records carrying this key in customMetadata or autoMetadata. Must be sent together with metadataValue; on its own it is a 400 INVALID_ARGUMENT.

metadataValuestringoptional

The 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.

Response

json
{
  "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.

bash
curl -sS -H "X-API-Key: $EC_API_KEY" \
  "https://api-next.expressconsent.com/v1/cdrs/cdr_abc123"
json
{
  "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

cdrIdstringrequired

The record identifier, the same value captureCDR() returned.

domainIdstringrequired

The base domain the record is filed under, and the value to use in the list path.

domainstringrequired

The domain recorded at capture.

organizationNamestringrequired

The name of your organization, as a convenience for rendering. Falls back to Organization when it cannot be read.

Timestamps

Both are Unix timestamps in milliseconds.

capturedAtnumberrequired

When 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.

createdAtnumberrequired

When we finished processing the record on our side. This is the field order sorts 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 put capturedAt later.

Evidence image

contentTypestringrequired

The image's MIME type.

sizenumberrequired

The image's size in bytes.

collectedbooleanrequired

Whether your organization has download access to this record. Two organizations asking about the same record legitimately get different answers. See access and collection.

downloadUrlstringoptional

A time-limited URL for the visual record, present only when collected is true. Do not store it; store the cdrId and request a fresh URL when you need one.

Expires 300 seconds after the response is generated.

Page context

pageUrlstringoptional

The URL of the captured page, including its query string. If you put personal data in query parameters, it is here.

clientTimezonestringoptional

The visitor's IANA time zone, as reported by their browser.

timeOnPageMsnumberoptional

How long the person had the page open before submitting. A useful signal for arguing a disclosure was readable rather than instantly dismissed.

signerTelemetryobjectoptional

Who submitted, as far as the network can say: ip and userAgent (either may be null), ipChain listing every address in the forwarding chain, and geo with an approximate location derived from the address. Best-effort: the whole object is absent when it could not be recorded.

detectedDisclosureDetailsobjectoptional

What 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.

userSubmitted is always true; the object's presence is the signal that a submission was observed, so test for the object rather than for this field. submitNodeId identifies the control that was pressed. derivedAt and deriverVersion record when the facts were derived and by which deriver, so a record can be re-derived from the stored capture and compared.

disclosures holds one entry per disclosure found, up to 50, each with:

  • key: the data-ec-disclosure value, or a generated one
  • text: the disclosure language read from the page, whitespace-collapsed and truncated at 5,000 characters
  • checkboxDetected: whether a consent checkbox was bound to it and readable
  • checkboxAffirmed: whether every readable checkbox bound to it was checked; present only when checkboxDetected is true
  • checkboxNodeId: the first bound checkbox element, for audit
  • consentMechanism: checkbox, button_submission, or none_detected

Read consentMechanism in preference to the booleans. It is absent only on records derived before the field existed.

disclosuresarrayoptional

Deprecated. The booleans an integrator asserted through the disclosures option, rather than anything we observed. Present only when that option was passed.

consentLanguagearrayoptional

Deprecated. Disclosure text captured by SDK versions predating derived detection.

Your metadata

customMetadataobjectoptional

Exactly what you passed as custom on the capture call.

autoMetadataobjectoptional

Fields we derived from the captured page, never anything you supplied. Present only when something was found, and filterable exactly like customMetadata.

phones, firstNames, and lastNames are sorted arrays read from identified form fields, with phones also picking up numbers found in page text and normalized to +1 form rather than stored as written. pageTitle and formAction come from the page itself. utmSource, utmMedium, utmCampaign, utmTerm, utmContent, gclid, fbclid, and msclkid come 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

sessionIdstringoptional

The session this capture belonged to. See sessions and Package CDRs.

packageIdstringoptional

The 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.

guestbooleanoptional

true when this record reached your account by being shared with you rather than captured on your own site.

producerOrgIdstringoptional

The 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

webhookSuccessfullySentAtnumberoptional

When 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.

bash
curl -sS -X POST -H "X-API-Key: $EC_API_KEY" \
  "https://api-next.expressconsent.com/v1/cdrs/cdr_abc123/collect"
json
{
  "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.

collectedbooleanrequired

Always true. The call either succeeds or returns an error.

alreadyCollectedbooleanrequired

Whether your organization could already download this record before the call. true means 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:

StatusCodeCause
400INVALID_ARGUMENTmetadataKey without metadataValue, a metadataKey and metadataValue too long to index, or a pageToken that is not a record on this domain
403PASSTHROUGH_ORG_CANNOT_COLLECTYour organization has no billing set up
404NOT_FOUNDNo 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.
410CDR_INVALIDThe 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.