Acceptance criteria
Declare what a shared record must prove before you will take it. Send acceptance on
POST /v1/shares/:token and a record that does not meet your
requirements is refused rather than delivered.
curl -sS -X POST -H "X-API-Key: $EC_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"acceptance": {
"schemaVersion": 1,
"record": { "maxAgeSeconds": 2592000 },
"disclosure": {
"requireUserSubmission": true,
"requireAllCheckboxesAffirmed": true,
"requiredText": [
{ "ruleId": "tcpa-autodialer", "anyOf": ["automated technology", "autodialer"] }
]
},
"leadMatch": { "phone": "(212) 555-1234" }
}
}' \
"https://api-next.expressconsent.com/v1/shares/xYz7Qw..."This is the check a lead buyer's compliance team would otherwise do by hand, after the record is already in their account and already paid for.
What a refusal costs you
Nothing. Evaluation happens before anything is written, so a refused record is not saved, you are not charged, and the token stays usable. You can retry with looser requirements, or tell the seller the lead does not meet your standard.
An accepted record is delivered exactly as it would have been without any criteria at all.
A rule that cannot be evaluated fails the same as a rule that is evaluated and not met. A record
captured with no detected submission carries no derived consent facts at all, so almost every
disclosure rule comes back unavailable and the record is refused. That is usually the answer you
want, but it means an empty-looking failure list is not a bug.
Shape
Every object here is strict, including the request body that carries it: an unrecognized or misspelled
field is a 400 INVALID_ARGUMENT rather than being ignored. That extends to the acceptance key itself,
so a typo fails loudly instead of quietly meaning "no requirements". The error's details.issues[] names
the offending path.
The smallest valid object is { "schemaVersion": 1 }, which imposes no requirements and accepts anything.
schemaVersion1requiredMust be the literal number
1. Not a default; omitting it is an error.recordobjectoptionalRequirements about the record itself.
disclosureobjectoptionalRequirements about what the person was shown and how they agreed. Where nearly all the substance is.
leadMatchobjectoptionalRequirement that the record's own form data matches the lead you were sold.
record
maxAgeSecondsintegeroptionalRefuse a record older than this.
Measured from when we finished processing the record, not from when the browser captured it. Those are normally seconds apart, but a capture whose first render failed and was recovered later carries a processing time well after its capture time. A record whose processing time is somehow in the future counts as zero seconds old and always passes.
Positive, whole, and at most 157,680,000 (5 years).
disclosure
Six booleans, plus two lists of text rules. Setting a boolean to true adds a check; omitting it, or
setting it to false, skips the check entirely.
requirePresentbooleanoptionalAt least one disclosure was found on the page at submission.
Weaker than it sounds: it asserts that disclosure language exists, not that anyone agreed to it. It does implicitly require a detected submission, since without one there are no derived facts to read at all and the check comes back
unavailable.requireUserSubmissionbooleanoptionalA submission was actually observed, rather than the capture being an arbitrary point in time.
The single most valuable check here. Without it you can accept a record that proves what a page said and nothing about anyone using it.
requireCheckboxPresentbooleanoptionalAt least one disclosure had a consent checkbox bound to it and readable.
Says a checkbox existed, not that it was checked. Pair it with the next one or use that instead.
requireAllCheckboxesAffirmedbooleanoptionalEvery checkbox that was found was checked. One unchecked box anywhere fails the record.
A record with no checkboxes at all is
unavailablerather than passing, because there is nothing to affirm, so this refuses consent-by-submission records too.requireEveryDisclosureConsentedbooleanoptionalEvery disclosure on the page was consented to by an accepted mechanism.
By default the only accepted mechanism is a checked checkbox. A record whose disclosure list is empty fails, since nothing was consented to; a record with no derived consent facts at all is
unavailable. This is the strictest of the booleans and usually the right one to reach for.allowConsentBySubmissionbooleanoptionalAlso accept consent expressed by the act of submitting, with no checkbox involved: the
button_submissionmechanism.That mechanism is only derived where the page carried no consent checkbox bound to the disclosure, no misconfigured consent checkbox anywhere, and a submit control whose label does not read as a refusal. So this does not accept a record where someone pressed "No", and it does not rescue a page with an orphaned checkbox.
It modifies
requireEveryDisclosureConsentedand nothing else, and the schema requiresrequireEveryDisclosureConsented: truewhenever this field is present at all, including when you set it tofalse. Setting it tofalsealongside that is valid and equivalent to omitting it.requiredTextarrayoptionalPhrases the disclosure language must contain. See text rules.
At most 20 rules. Rule identifiers must be unique across requiredText and forbiddenText together.forbiddenTextarrayoptionalPhrases the disclosure language must not contain. See text rules.
At most 20 rules, sharing the same identifier namespace as requiredText.
Every one of these reads only the consent facts we derived at a detected submission. None of them can be
satisfied by a boolean the integrator asserted through the deprecated disclosures option, which is the
point: your requirements are checked against what was observed, not against what the seller asserts.
Text rules
A rule matches phrases against the disclosure text stored on the record.
ruleIdstringrequiredYour name for the rule. It comes back verbatim in the failure list, so make it something you can recognize in a log six months from now.
1 to 64 characters matching [A-Za-z0-9._-]. Must not begin with record., disclosure., or leadMatch. Those prefixes are reserved for built-in checks.anyOfstring[]requiredCandidate phrases. Any one matching satisfies the rule, so use this for the variants of a phrase you consider equivalent.
1 to 10 phrases, each 1 to 5,000 characters after trimming.matchstringoptionaldefaultcontainscontainslooks for the phrase inside the disclosure text.exactrequires the whole disclosure text to equal the phrase, which is only realistic when you know the seller's exact wording.contains or exact.scopestringoptionaldefaultany_disclosureany_disclosurepasses when at least one disclosure matches.every_disclosurerequires every disclosure on the page to match. Neither passes a record whose disclosure list is empty:every_disclosurefails it outright, andany_disclosurehas nothing to match against.On a page with a TCPA disclosure and a separate marketing-email disclosure,
every_disclosuredemands your phrase appear in both.any_disclosureis almost always what you mean.any_disclosure or every_disclosure. On requiredText only; forbiddenText has no scope.
A forbiddenText rule passes when no disclosure matches any of its phrases, including on a record with no
disclosures at all, since nothing can match. Use it to refuse language you know your compliance team
rejects. Like every other disclosure rule it still comes back unavailable on a record with no derived
consent facts, and refuses it.
How matching works
Both the disclosure text and your phrases are normalized before comparison: Unicode-normalized, runs of whitespace collapsed to single spaces, trimmed, and lowercased. So matching is case-insensitive and indifferent to line breaks or indentation in the captured markup.
Most punctuation is left alone. A curly apostrophe does not match a straight one, so prefer phrases without
punctuation: automated technology rather than we'll use automated technology.
leadMatch
phonestringrequiredRefuse the record unless this phone number appears in the captured form.
Any common formatting works.
(212) 555-1234,212-555-1234, and+12125551234all normalize to the same number.1 to 40 characters, and must normalize to a 10-digit North American number with an optional leading 1. A value that does not is a 400 INVALID_ARGUMENT, not a rule failure, and it carries no details.issues.
This is the check that catches a mismatched handoff: the lead record says one number and the consent evidence shows another. Send the number the seller gave you and the record is refused unless the person in the evidence is the person you were sold.
Your number is compared as a one-way hash against hashes computed from the record when it was processed. The number you send is never stored.
The record's hashes come from form fields identified as phone inputs: a tel type, a phone autocomplete
hint, or a name or id suggesting one. A number typed into an unlabeled field, or appearing only in the
disclosure text or page copy, is not matched, and neither is one that only reached the page after the
capture. On a form we could not read a phone field from, this check is unavailable and the record is
refused.
Failures
A record that does not meet your requirements comes back as 422 CDR_ACCEPTANCE_FAILED, with every check
that did not pass listed.
{
"ok": false,
"error": {
"code": "CDR_ACCEPTANCE_FAILED",
"message": "The CDR did not meet the supplied acceptance requirements.",
"details": {
"outcome": "rejected",
"failures": [
{ "ruleId": "disclosure.requireUserSubmission", "status": "failed", "code": "USER_SUBMISSION_NOT_DETECTED" },
{ "ruleId": "tcpa-autodialer", "status": "unavailable", "code": "DISCLOSURE_DETAILS_UNAVAILABLE" }
]
},
"requestId": "3f2504e0-4f89-11d3-9a0c-0305e82c3301"
}
}Each entry has exactly three fields. Built-in checks report the dotted name of the field that requested
them; text rules report your own ruleId.
ruleIdstringrequiredWhich requirement this is about.
statusstringrequiredfailedmeans the check ran and the record did not meet it.unavailablemeans the record lacks the data the check needs. Both refuse the record. Only checks that passed are absent from the list.codestringrequiredA stable machine-readable reason, from the table below.
Only the failures are returned; checks that passed are not listed. A single unmet requirement is enough to refuse the whole record.
Codes
| Code | Status | Meaning |
|---|---|---|
RECORD_TOO_OLD | failed | Older than maxAgeSeconds |
DISCLOSURE_NOT_PRESENT | failed | No disclosure was found on the page |
USER_SUBMISSION_NOT_DETECTED | failed | No submission was observed |
CHECKBOX_NOT_PRESENT | failed | No readable consent checkbox was bound to any disclosure |
CHECKBOX_NOT_AFFIRMED | failed | A checkbox was found and at least one was unchecked |
CHECKBOX_AFFIRMATION_UNAVAILABLE | unavailable | No checkbox anywhere, so there was nothing to affirm |
DISCLOSURE_CONSENT_NOT_ACCEPTED | failed | A disclosure was consented to by a mechanism you did not accept, or not at all |
REQUIRED_DISCLOSURE_TEXT_NOT_FOUND | failed | A requiredText rule matched nothing in scope |
FORBIDDEN_DISCLOSURE_TEXT_FOUND | failed | A forbiddenText rule matched |
LEAD_PHONE_NOT_MATCHED | failed | The number is not in the record |
LEAD_PHONE_MATCH_UNAVAILABLE | unavailable | The record carries no phone data to compare against |
DISCLOSURE_DETAILS_UNAVAILABLE | unavailable | The record has no derived consent facts, so this check could not run |
DISCLOSURE_DETAILS_UNAVAILABLE is the one to expect most. It means no submission was detected on that
capture, and it appears once for every disclosure rule you set except two. requireUserSubmission reports
USER_SUBMISSION_NOT_DETECTED instead, because the absence of derived facts is the answer to the question
it asks. requireAllCheckboxesAffirmed reports CHECKBOX_AFFIRMATION_UNAVAILABLE instead, because it cannot tell
"no checkbox on the page" apart from "no derived facts at all", and gives the same code for both.
There is a matching code for each check that passes: RECORD_AGE_ACCEPTED, LEAD_PHONE_MATCHED, and so
on. You will not see them, since only failures are returned.
A reasonable starting policy
For a buyer who calls the numbers on the leads they buy:
{
"schemaVersion": 1,
"record": { "maxAgeSeconds": 7776000 },
"disclosure": {
"requireUserSubmission": true,
"requireEveryDisclosureConsented": true
},
"leadMatch": { "phone": "(212) 555-1234" }
}That refuses anything not tied to an observed submission, anything where a disclosure was not affirmatively
agreed to, anything over 90 days old, and anything where the evidence is about a different person. Add
requiredText rules once you know which phrases your own counsel insists on.
Expect this to reject records at first. A seller whose pages produce records that fail
requireUserSubmission has an integration problem worth telling them about. The fixes are on
Tagging a form.