API reference
Server-to-server endpoints for listing, collecting, sharing, and downloading consent evidence.
On this page
API keys must never appear in browser/client-side code. Treat them like passwords: store as environment secrets on your server.
Authentication
Generate an API key in the dashboard (Organization → API Keys). The secret is shown only once — if lost, generate a new key.
Send it as X-API-Key in the format <keyId>.<secret>:
X-API-Key: <keyId>.<secret>Base URL
export EC_API_KEY="<keyId>.<secret>"
# Production:
export EC_API_BASE_URL="https://api-next.expressconsent.com"All curl examples below use $EC_API_BASE_URL so they work in any environment.
Response format
All responses are JSON with a request ID for debugging.
Success
{
"ok": true,
"data": { ... },
"requestId": "2e9f4a2d-..."
}Error
{
"ok": false,
"error": {
"code": "UNAUTHENTICATED",
"message": "API key required",
"requestId": "2e9f4a2d-..."
}
}Error codes:
UNAUTHENTICATED(401) — missing, malformed, or invalid API keyFORBIDDEN(403) — authenticated but not authorizedINVALID_ARGUMENT(400) — bad query/path inputNOT_FOUND(404) — resource doesn’t exist or isn’t accessibleMETHOD_NOT_ALLOWED(405) — wrong HTTP method for endpointCONFLICT(409) — conflicting state (e.g. duplicates)GONE(410) — resource expired (e.g. share token past expiry)CDR_INVALID(410) — CDR has been administratively invalidated; treat likeNOT_FOUNDfor evidence purposesFAILED_PRECONDITION(412) — action requires a prior step
Endpoints
Read endpoints
GET /v1/domains
List all domains for your organization.
curl -sS \
-H "X-API-Key: $EC_API_KEY" \
"$EC_API_BASE_URL/v1/domains"GET /v1/domains/:domainId/cdrs
List CDRs for a domain, with pagination and optional metadata filtering.
pageSize: 1–100 (default 20)order:asc|desc(defaultdesc)pageToken: CDR ID to start aftermetadataKey+metadataValue: filter by custom metadata (both required together)
curl -sS \
-H "X-API-Key: $EC_API_KEY" \
"$EC_API_BASE_URL/v1/domains/example.com/cdrs?pageSize=20&order=desc"curl -sS \
-H "X-API-Key: $EC_API_KEY" \
"$EC_API_BASE_URL/v1/domains/example.com/cdrs?metadataKey=leadId&metadataValue=123"When the SDK’s initial upload fails, it retries automatically on the user’s next page load. Use metadata filtering to find CDRs that uploaded via retry for lead records missing a CDR ID. Pass a unique identifier in captureCDR({ custom: { uid: '...' } }), then query with metadataKey=uid&metadataValue=... to find the CDR. See Offline resilience for the full reconciliation pattern.
Example response
{
"ok": true,
"data": {
"cdrs": [
{
"cdrId": "abc_123",
"domainId": "example.com",
"domain": "example.com",
"createdAt": 1738600000000,
"contentType": "image/jpeg",
"size": 276472,
"collected": true,
"downloadUrl": "https://storage.googleapis.com/... (short-lived)",
"customMetadata": { "leadId": "123" },
"sessionId": "session_abc",
"subGroupIds": ["step-1"]
}
],
"nextPageToken": "abc_122"
}
}GET /v1/cdrs/:cdrId
Fetch a single CDR with full detail, including signer telemetry (IP, User-Agent, geo).
For CDRs received via sharing, the response includes guest: true and producerOrgId indicating the original producing organization.
curl -sS \
-H "X-API-Key: $EC_API_KEY" \
"$EC_API_BASE_URL/v1/cdrs/abc_123"Example response
{
"ok": true,
"data": {
"cdr": {
"cdrId": "abc_123",
"domainId": "example.com",
"domain": "example.com",
"organizationName": "Acme Corp",
"capturedAt": 1738600000000,
"createdAt": 1738600000000,
"contentType": "image/jpeg",
"size": 276472,
"collected": true,
"downloadUrl": "https://storage.googleapis.com/...",
"pageUrl": "https://example.com/consent",
"signerTelemetry": {
"ip": "203.0.113.1",
"ipChain": ["203.0.113.1"],
"userAgent": "Mozilla/5.0 ...",
"geo": {
"countryCode": "US",
"region": "CA",
"city": "Los Angeles",
"latitude": 34.0522,
"longitude": -118.2437,
"accuracyRadiusKm": 20,
"source": "maxmind"
}
},
"customMetadata": { "leadId": "123" },
"disclosures": [
{
"key": "tcpa",
"language": "By submitting this form, you consent to receive calls via autodialer...",
"agreed": true
}
],
"sessionId": "session_abc"
}
}
}Write endpoints
POST /v1/cdrs/:cdrId/collect
Pay for a CDR for your organization. After a successful collect, your org is billed for this CDR and gets full download access. Idempotent for your org — calling twice returns alreadyCollected: true.
Multiple organizations can independently collect the same CDR (each pays their own bill and gets their own access). One org collecting does not unlock access for other orgs.
curl -sS -X POST \
-H "X-API-Key: $EC_API_KEY" \
"$EC_API_BASE_URL/v1/cdrs/abc_123/collect"Example response
{
"ok": true,
"data": {
"cdrId": "abc_123",
"collected": true,
"alreadyCollected": false
}
}POST /v1/cdrs/:cdrId/share
Generate a share URL for a CDR. The returned shareUrl can be given to a business partner (e.g. a lead buyer) so they can receive the evidence. The CDR does not need to be collected first — either you or the recipient can collect (pay for) the CDR at any time. If you have already paid, your recipients get free download access when they POST to the share URL.
Optional body: { "expiresInMs": 3600000 } (default: 30 days, max: 2 years). Each call generates a new unique token.
If you’re generating leads from a website, use captureCDR({ autoShare: true }) instead — it returns the share URL in the same call with no extra API request. This endpoint is for server-side share generation when you need to share after the fact.
curl -sS -X POST \
-H "X-API-Key: $EC_API_KEY" \
-H "Content-Type: application/json" \
-d '{"expiresInMs":3600000}' \
"$EC_API_BASE_URL/v1/cdrs/abc_123/share"Example response
{
"ok": true,
"data": {
"token": "share_token_xyz",
"shareUrl": "/v1/shares/share_token_xyz",
"expiresAt": 1739200000000
}
}POST /v1/shares/:token
Open a share URL with your org’s API key. By default this is a single-step collect: the CDR is added to your org and your org is billed for it. If the org that created the share has already paid for the CDR, you are not billed — free download access is granted instead.
When using autoShare, the SDK returns a full absolute shareUrl — the recipient POSTs to that URL with their API key. When using the server-side share endpoint, prepend your API base URL to the returned path. The legacy path /v1/shares/:token/claim remains supported for backward compatibility and behaves identically.
Idempotent — POSTing the same URL twice from the same org returns alreadyClaimed: true on the second call and never bills twice. You cannot open a share URL you generated yourself (returns 400 INVALID_ARGUMENT). Expired tokens return 410 GONE.
curl -sS -X POST \
-H "X-API-Key: $EC_API_KEY" \
"$EC_API_BASE_URL/v1/shares/share_token_xyz"Example response
{
"ok": true,
"data": {
"claimed": true,
"alreadyClaimed": false,
"cdrId": "abc_123",
"domainId": "example.com",
"shareEventId": "se_abc123def456"
}
}Save without paying (opt-out)
To save the CDR to your org without billing yourself, pass { "collect": false }. The CDR shows up in your account but you cannot download it until you collect later via POST /v1/cdrs/:cdrId/collect. If the org that created the share has already paid, you receive free download access regardless of collect: false — the sharer wanted you to have it.
# Save the share to your org without billing yourself.
# Use when you only want visibility (no download access yet).
curl -sS -X POST \
-H "X-API-Key: $EC_API_KEY" \
-H "Content-Type: application/json" \
-d '{"collect": false}' \
"$EC_API_BASE_URL/v1/shares/share_token_xyz"Collected vs Pending
The collected field on every CDR response is per-organization: it reflects whether your org has download access to this CDR. Two orgs viewing the same CDR can see different collected values.
Your org has collected: true for a CDR when either of these is true:
- Your org paid for it — via auto-collect at capture time, or by POSTing to a share URL (which collects by default), or via POST /v1/cdrs/:cdrId/collect.
- Your org received it via a share URL from an org that has already paid — you get free download access automatically (no billing). See POST /v1/shares/:token.
Notes:
downloadUrlis only returned whencollected === truefor your org.- Download URLs are short-lived signed URLs (~5 min). Store
cdrId, not the URL. - A single payment from one org enables that org to share with any number of partners, and every partner gets free download access. Free access flows one hop forward; if partners then re-share to their downstream partners, those downstream recipients collect (pay) on their own POST.
By default, new organizations have auto-collect enabled — CDRs your org captures via the SDK are automatically collected (your org is billed) as soon as they are created. Most lead-generation businesses prefer the buyer to pay; if that fits your model, disable auto-collect for your organization.
Package CDR API Coming soon
Package CDRs bundle multiple CDRs from the same user session into a composite evidence document. The following endpoints will enable programmatic access to Package CDRs, including sharing entire multi-step consent flows in a single operation.
GET /v1/packages/:packageId— Get Package CDR details (all CDRs, session context, telemetry)POST /v1/packages/:packageId/share— Share a Package CDR (generate a share link for the entire session)POST /v1/package-shares/:token— Open a Package CDR share URL (single-step collect for the whole bundle)
Until these endpoints are available, share individual CDRs using POST /v1/cdrs/:cdrId/share. When a buyer receives multiple CDRs from the same session, they appear together in the buyer’s Package CDRs view. See the Package CDRs documentation for more details.
Related docs
- Sharing evidence with lead buyers — autoShare, opening share URLs, and the full lead generator / lead buyer flow
- Webhooks — receive real-time notifications when CDRs are rendered
- Quickstart
- Troubleshooting