# Acceptance criteria

Declare what a shared record must prove before you will take it. Send `acceptance` on
[`POST /v1/shares/:token`](https://app.expressconsent.com/docs/reference/api/shares) 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.

> **Warning: A rule that cannot be evaluated refuses the record**
>
> 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.

**`schemaVersion`** (`1`, required)

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

**`record`** (`object`, optional)

Requirements about the record itself.

**`disclosure`** (`object`, optional)

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

**`leadMatch`** (`object`, optional)

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

## record

**`maxAgeSeconds`** (`integer`, optional)

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.

Constraints: 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.

**`requirePresent`** (`boolean`, optional)

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

**`requireUserSubmission`** (`boolean`, optional)

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.

**`requireCheckboxPresent`** (`boolean`, optional)

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.

**`requireAllCheckboxesAffirmed`** (`boolean`, optional)

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.

**`requireEveryDisclosureConsented`** (`boolean`, optional)

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.

**`allowConsentBySubmission`** (`boolean`, optional)

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.

**`requiredText`** (`array`, optional)

Phrases the disclosure language must contain. See [text rules](#text-rules).

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

**`forbiddenText`** (`array`, optional)

Phrases the disclosure language must not contain. See [text rules](#text-rules).

Constraints: 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.

**`ruleId`** (`string`, required)

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.

Constraints: 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.

**`anyOf`** (`string[]`, required)

Candidate phrases. Any one matching satisfies the rule, so use this for the variants of a phrase you
consider equivalent.

Constraints: 1 to 10 phrases, each 1 to 5,000 characters after trimming.

**`match`** (`string`, optional, default `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.

Constraints: contains or exact.

**`scope`** (`string`, optional, default `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.

Constraints: 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

**`phone`** (`string`, required)

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.

Constraints: 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.

> **Info: Only numbers from phone form fields count**
>
> 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.

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

**`ruleId`** (`string`, required)

Which requirement this is about.

**`status`** (`string`, required)

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

**`code`** (`string`, required)

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

| 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:

```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](https://app.expressconsent.com/docs/tags).
