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.

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

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.

schemaVersion1required

Must be the literal number 1. Not a default; omitting it is an error.

recordobjectoptional

Requirements about the record itself.

disclosureobjectoptional

Requirements about what the person was shown and how they agreed. Where nearly all the substance is.

leadMatchobjectoptional

Requirement that the record's own form data matches the lead you were sold.

record

maxAgeSecondsintegeroptional

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

requirePresentbooleanoptional

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

requireUserSubmissionbooleanoptional

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

requireCheckboxPresentbooleanoptional

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

requireAllCheckboxesAffirmedbooleanoptional

Every checkbox that was found was checked. One unchecked box anywhere fails the record.

A record with no checkboxes at all is unavailable rather than passing, because there is nothing to affirm, so this refuses consent-by-submission records too.

requireEveryDisclosureConsentedbooleanoptional

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

allowConsentBySubmissionbooleanoptional

Also accept consent expressed by the act of submitting, with no checkbox involved: the button_submission mechanism.

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 requireEveryDisclosureConsented and nothing else, and the schema requires requireEveryDisclosureConsented: true whenever this field is present at all, including when you set it to false. Setting it to false alongside that is valid and equivalent to omitting it.

requiredTextarrayoptional

Phrases the disclosure language must contain. See text rules.

At most 20 rules. Rule identifiers must be unique across requiredText and forbiddenText together.
forbiddenTextarrayoptional

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

ruleIdstringrequired

Your 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[]required

Candidate 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.
matchstringoptionaldefault contains

contains looks for the phrase inside the disclosure text. exact requires the whole disclosure text to equal the phrase, which is only realistic when you know the seller's exact wording.

contains or exact.
scopestringoptionaldefault any_disclosure

any_disclosure passes when at least one disclosure matches. every_disclosure requires every disclosure on the page to match. Neither passes a record whose disclosure list is empty: every_disclosure fails it outright, and any_disclosure has nothing to match against.

On a page with a TCPA disclosure and a separate marketing-email disclosure, every_disclosure demands your phrase appear in both. any_disclosure is 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

phonestringrequired

Refuse the record unless this phone number appears in the captured form.

Any common formatting works. (212) 555-1234, 212-555-1234, and +12125551234 all 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.

Failures

A record that does not meet your requirements comes back as 422 CDR_ACCEPTANCE_FAILED, with every check that did not pass listed.

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

ruleIdstringrequired

Which requirement this is about.

statusstringrequired

failed means the check ran and the record did not meet it. unavailable means the record lacks the data the check needs. Both refuse the record. Only checks that passed are absent from the list.

codestringrequired

A 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

CodeStatusMeaning
RECORD_TOO_OLDfailedOlder than maxAgeSeconds
DISCLOSURE_NOT_PRESENTfailedNo disclosure was found on the page
USER_SUBMISSION_NOT_DETECTEDfailedNo submission was observed
CHECKBOX_NOT_PRESENTfailedNo readable consent checkbox was bound to any disclosure
CHECKBOX_NOT_AFFIRMEDfailedA checkbox was found and at least one was unchecked
CHECKBOX_AFFIRMATION_UNAVAILABLEunavailableNo checkbox anywhere, so there was nothing to affirm
DISCLOSURE_CONSENT_NOT_ACCEPTEDfailedA disclosure was consented to by a mechanism you did not accept, or not at all
REQUIRED_DISCLOSURE_TEXT_NOT_FOUNDfailedA requiredText rule matched nothing in scope
FORBIDDEN_DISCLOSURE_TEXT_FOUNDfailedA forbiddenText rule matched
LEAD_PHONE_NOT_MATCHEDfailedThe number is not in the record
LEAD_PHONE_MATCH_UNAVAILABLEunavailableThe record carries no phone data to compare against
DISCLOSURE_DETAILS_UNAVAILABLEunavailableThe 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:

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