{
  "layer": "CAMPAIGN",
  "name": "Campaign",
  "url": "https://ekas.io/rules/10dlc/campaign/",
  "updated": "2026-07-25",
  "licence": "CC BY 4.0 — https://creativecommons.org/licenses/by/4.0/",
  "count": 94,
  "rules": [
    {
      "id": "CMP-003",
      "slug": "cmp-003",
      "title": "Message flow must not begin or end with whitespace",
      "statement": "The opt-in / message-flow field must carry no leading or trailing whitespace.",
      "rationale": "A newline pasted in front of the text, or a run of spaces left after it, is invisible in the textarea it was typed into — and the validator rejects the field rather than trimming it, naming a field the submitter can see nothing wrong with. It is almost always an artifact of copying the flow out of a document, so the author has no memory of doing anything that would explain the error.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.message_flow",
      "severity": "BLOCKING",
      "detectability": [
        "DETERMINISTIC"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "AWS"
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Strip the blank first line and any trailing spaces or newline before submitting. Done when the value starts on the first character of the first word and stops on the final full stop — paste it into a plain-text editor if you cannot see the difference.",
      "example": "message_flow begins at \"Customers opt in at https://acmecoffee.com/checkout…\" and ends at \"…Screenshot: https://cdn.acmecoffee.com/compliance/optin.png\" — nothing before the C, nothing after the g.",
      "notes": "Catalog CMP-003 records this as AWS form validation only. Kept universal rather than provider-tagged because trimming a field costs nothing anywhere else, and the strict-superset model prefers a free precaution to a provider-conditional rule that silently stops firing when the submission target is unknown.",
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-010",
      "slug": "cmp-010",
      "title": "HELP and STOP replies should fit in a single 160-character segment",
      "statement": "The HELP and opt-out reply bodies should each fit within 160 characters.",
      "rationale": "A HELP or STOP reply over 160 characters is delivered as a multi-part message, and the parts can arrive out of order or be dropped independently — so the recipient asking for help receives half an answer, or an opt-out confirmation that reads as a fresh marketing message. The 320-character general bound tempts authors into two segments for replies that gain nothing from the extra room.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.help_message + campaign.optout_message",
      "severity": "MEDIUM",
      "detectability": [
        "DETERMINISTIC"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "AWS"
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Cut the reply to one segment by dropping the marketing sentence rather than the disclosures — the brand name, the contact route and the STOP instruction all have to stay. Done when both replies are at or under 160 characters.",
      "example": "help_message: \"Acme Coffee Rewards: for help email support@acmecoffee.com or call 415-555-0134. Reply STOP to unsubscribe.\" (110 characters)",
      "notes": "AWS's registration form enforces 160 on these two bodies, tighter than the 320 that CMP-AUTOREPLY-LEN applies from Twilio and Bandwidth. Graded MEDIUM and worded as a fit-in-one-segment rule because the 320 bound is what most providers reject on; this one catches the message that will be split in delivery.",
      "catalogIds": [
        "MSG-013",
        "MSG-016"
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-014",
      "slug": "cmp-014",
      "title": "Keyword entries must carry no surrounding whitespace",
      "statement": "Keyword lists must be comma-separated with no space around the individual entries.",
      "rationale": "Keyword lists are stored as one comma-delimited string and split back apart without trimming, so a space typed after a comma becomes part of the next keyword — and \" STOP\" is not STOP. The consumer texts the word on your signage, the match fails, and the opt-out that everyone believes is configured never fires.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.optin_keywords + optout_keywords + help_keywords",
      "severity": "MEDIUM",
      "detectability": [
        "DETERMINISTIC"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "Plivo",
        "Bandwidth",
        "TCR",
        "Telnyx"
      ],
      "codes": [
        {
          "provider": "TCR",
          "code": "501",
          "remediable": true
        }
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Retype the lists with commas and no spaces, or trim each entry if you are building the string in code. Done when no keyword begins or ends with a space — which you can only see by selecting the text, so check it in code rather than by eye.",
      "example": "optout_keywords: STOP,QUIT,END,REVOKE,OPTOUT,CANCEL,UNSUBSCRIBE",
      "notes": "A space INSIDE a keyword is a different defect with a different fix and belongs to CMP-KEYWORD-FMT, which rejects the whole token as non-alphanumeric. This rule is only about padding introduced by splitting the delimited string, so the two do not report the same list twice.",
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-016",
      "slug": "cmp-016",
      "title": "Contact email and phone must contain no spaces",
      "statement": "The registration contact email and phone number must not contain spaces anywhere.",
      "rationale": "A space inside a contact address is the single most common field-hygiene rejection, and it survives because the space is usually trailing — copied along with the address out of a spreadsheet cell or an email signature. The address looks right in the form and is unusable to anything that parses it.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign contact email + phone (the brand contact, where the CSP defaults it)",
      "severity": "HIGH",
      "detectability": [
        "DETERMINISTIC"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "Bandwidth",
        "AWS"
      ],
      "codes": [
        {
          "provider": "Bandwidth/DCA",
          "code": "6102",
          "remediable": true
        }
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Retype the contact email and phone rather than pasting them, or trim the value in a plain-text editor first. Done when neither field contains a space at the start, the end, or in the middle.",
      "example": "contact email: support@acmecoffee.com · contact phone: 415-555-0134",
      "notes": "Bandwidth 6102 verbatim: \"The email and/or phone number must not contain any spaces.\" The catalog states this row more broadly as \"spaces or special characters\"; that wording traces to a secondary reseller paraphrase of the same code, so only the spaces half is asserted here. Separators inside a phone number are governed by the E.164 formatting rule on the brand, not by this one.",
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-017",
      "slug": "cmp-017",
      "title": "Every mandatory campaign attribute must be explicitly set",
      "statement": "The mandatory campaign boolean attributes — embeddedLink, embeddedPhone, ageGated, directLending and numberPool — must each be explicitly true or false, never left unset.",
      "rationale": "An unset boolean is not the same as false: providers reject nulls on required attributes rather than defaulting them, because the declaration is the thing being relied on downstream. It is also the signature failure of a form filled by an integration that only sends changed fields, so the brand sees a rejection for a field they never knowingly left blank.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign attributes",
      "severity": "BLOCKING",
      "detectability": [
        "DETERMINISTIC"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "TCR",
        "Bandwidth",
        "AWS"
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Set every attribute explicitly to true or false rather than leaving it unset — including the ones that are false. Done when your submission payload carries all five keys with boolean values, not when the form merely looks complete.",
      "example": "embeddedLink: false · embeddedPhone: false · ageGated: false · directLending: false · numberPool: false",
      "pitfalls": [
        "A PATCH-style integration that sends only changed fields will drop every attribute the user did not touch, which is why this fails most often on resubmission rather than first submission."
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-018",
      "slug": "cmp-018",
      "title": "Attributes that do not apply to the registration type must be empty",
      "statement": "Conditionally-allowed fields must not be populated when they do not apply to the chosen registration type.",
      "rationale": "Populating a field the registration type does not have produces a contradictory record, and validation rejects contradictions rather than ignoring surplus data. It typically happens when a form is cloned from a different brand tier.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign attributes",
      "severity": "MEDIUM",
      "detectability": [
        "DETERMINISTIC"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "TCR",
        "Twilio"
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Clear attributes that do not apply to this registration type before submitting — notably number pooling on a sole proprietor campaign, which is limited to a single number.",
      "example": "Sole proprietor campaign: number pool false, single number only.",
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-019",
      "slug": "cmp-019",
      "title": "Policy link fields must be 255 characters or fewer",
      "statement": "The privacy policy and terms links must each be at most 255 characters.",
      "rationale": "A link that overruns the field is stored truncated, so the reviewer follows a URL that 404s and reads it as a policy page that does not exist. The values that overrun are almost always tracking-parameter tails on an otherwise short URL, which carry nothing a reviewer needs.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.privacy_url + campaign.terms_url",
      "severity": "LOW",
      "detectability": [
        "DETERMINISTIC"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "Bandwidth",
        "TCR"
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Link the bare page and drop everything from the \"?\" onward — campaign tags and session parameters serve no purpose here. Done when both links open the right page from a private browsing window and each is comfortably under 255 characters.",
      "example": "https://acmecoffee.com/privacy — not https://acmecoffee.com/privacy?utm_source=tcr&utm_campaign=…",
      "notes": "TCR's own schema allows 2048; the Bandwidth CSP API caps at 255, and 255 is the portable value. Measures whichever of the two links is longer, so one finding names the field that will actually overrun.",
      "catalogIds": [
        "CMP-020",
        "POL-004"
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-021",
      "slug": "cmp-021",
      "title": "Reference id must be 50 characters or fewer",
      "statement": "The campaign reference id must be at most 50 characters, and unique within the CSP.",
      "rationale": "The reference id is your own key for finding this campaign again when a rejection arrives months later, and an over-long value is rejected at submission rather than truncated. Teams reach the cap by concatenating a customer name, a use case and a timestamp into one string, which is also what makes it collide with a sibling campaign.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.reference_id",
      "severity": "MEDIUM",
      "detectability": [
        "DETERMINISTIC"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "TCR",
        "Bandwidth",
        "Telnyx"
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Use a short opaque key you can look up on your side rather than a descriptive sentence, and confirm no other campaign in your CSP account already uses it. Done when the value is under 50 characters and unique.",
      "example": "reference_id: SP-2026-0142",
      "notes": "Only the length half is settled here. Uniqueness needs the CSP's whole campaign list, which a single registration does not carry — a duplicate surfaces at submission as \"Duplicate campaign\", and the remediation says to check it because we cannot.",
      "catalogIds": [
        "BRD-179"
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-023",
      "slug": "cmp-023",
      "title": "Declared embedded links must come with a sample URL",
      "statement": "A campaign declaring embedded links must supply the example link, at most 255 characters and at most fifteen entries.",
      "rationale": "The declared link sample is what a reviewer screens for a shortener, a mismatched domain, or a destination that has nothing to do with the brand — so declaring that your messages carry links and then supplying none leaves the most-screened element of the campaign unreviewable. It fails on resubmission more often than first submission, because the attribute is flipped to true after a rejection and the sample field is not filled in with it.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.embedded_link_sample",
      "severity": "HIGH",
      "detectability": [
        "DETERMINISTIC"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "TCR",
        "Vonage"
      ],
      "codes": [
        {
          "provider": "TCR",
          "code": "501",
          "remediable": true
        }
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Paste a real link your messages will contain into the URL sample field — the live destination, on your own domain, not a placeholder or a shortener. Done when every distinct link domain your campaign sends appears in the list.",
      "example": "embeddedLink: true · embeddedLinkSample: https://acmecoffee.com/sale",
      "notes": "One field, two provider names: TCR takes a single embeddedLinkSample, Vonage takes a URL Samples list of up to fifteen (catalog CMP-171). The obligation is the same in both, so it is one rule reading one input rather than two rules differing only in arity.",
      "catalogIds": [
        "CMP-171"
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-024",
      "slug": "cmp-024",
      "title": "Registration fields must be written in English",
      "statement": "Every campaign registration field must be written in English.",
      "rationale": "US reviewers assess in English, and a field they cannot read cannot be checked against any requirement — so a non-English field is not judged leniently, it is rejected. Sending messages in other languages is entirely fine; the registration record is the only thing that must be readable.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.* (all text fields)",
      "severity": "BLOCKING",
      "detectability": [
        "AI_FORM"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "TCR",
        "Twilio",
        "Bandwidth",
        "AWS"
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Write the description and message flow in English. Where samples are genuinely sent in another language, keep the original and add an English translation beside it in the same field. Done when every field can be read end to end by an English-only reviewer.",
      "example": "Sample 1: \"Acme Coffee: 20% de descuento este fin de semana. Responde STOP para cancelar.\" / English: \"Acme Coffee: 20% off this weekend. Reply STOP to opt out.\"",
      "pitfalls": [
        "Translating the samples but leaving the message flow in the original language fails the same rule — Bandwidth checks the CTA, flow, and samples separately."
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-028",
      "slug": "cmp-028",
      "title": "Opt-in evidence must be a JPG, PNG or PDF under 500 KB",
      "statement": "Uploaded opt-in evidence must be a JPG, PNG or PDF file no larger than 500 KB.",
      "rationale": "The upload field rejects anything else outright, and the two formats people actually reach for are the two that fail: a phone screenshot saved as HEIC, and a full-resolution PNG straight off a retina display that is several megabytes. Both look like ordinary screenshots to the person attaching them, so the rejection reads as arbitrary.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign opt-in evidence upload",
      "severity": "BLOCKING",
      "detectability": [
        "DETERMINISTIC"
      ],
      "failureClass": "TERMINAL_EVIDENCE",
      "authorities": [
        "AWS",
        "TextUs"
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Export the screenshot as PNG or JPG — on iOS, change Settings › Camera › Formats to \"Most Compatible\", or open the file and re-save it — and shrink it below 500 KB by cropping to the consent surface rather than the whole page. Done when the file opens in a browser and its size reads under 500 KB.",
      "pitfalls": [
        "Cropping to reduce the size is the right move only until it cuts off the disclosure text; a small screenshot that no longer shows what the consumer agreed to trades this failure for a worse one."
      ],
      "notes": "Verbatim from the AWS campaign registration form: \"The supported file type are PNG, JPEG, and PDF and the maximum file size is 500KB.\" Size is checked only where the registration carries upload metadata; a bare hosted URL gives us the file type from its extension and nothing about its weight, and the check says which of the two it settled rather than implying it checked both.",
      "catalogIds": [
        "CMP-029",
        "CON-022",
        "OPS-330"
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-035",
      "slug": "cmp-035",
      "title": "The description must describe this programme, not messaging in general",
      "statement": "The campaign description must describe this specific programme rather than restating that the business sends messages.",
      "rationale": "A description that would fit any business tells a reviewer nothing they can check against the samples, the use case or the opt-in, so there is no way to approve it and the only safe verdict is refusal. It is rarely laziness: the field is presented as an administrative box rather than as the case for the campaign, so people answer the question they think was asked — \"do you send texts?\" — instead of the one that was.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.description",
      "severity": "BLOCKING",
      "detectability": [
        "AI_FORM"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "Bandwidth",
        "Plivo",
        "Vonage",
        "Sinch",
        "HighLevel"
      ],
      "codes": [
        {
          "provider": "Bandwidth",
          "code": "2002",
          "remediable": true
        },
        {
          "provider": "Bandwidth",
          "code": "2001",
          "remediable": true
        }
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Rewrite the description naming your business, the people who receive the messages, what those messages contain, and where those people opted in. Done when swapping in a competitor's name would make the sentence false.",
      "example": "Acme Coffee sends weekly promotional offers, seasonal sale notifications and rewards balance updates to customers who ticked the marketing consent box at checkout on acmecoffee.com.",
      "pitfalls": [
        "Padding a generic description to clear the 40-character floor makes it longer without making it specific, and length is not what is being graded."
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-037",
      "slug": "cmp-037",
      "title": "The description must name the business that sends the messages",
      "statement": "The campaign description must name the business whose messages these are, not the platform or agency registering on its behalf.",
      "rationale": "The consumer who receives the message consented to hear from a particular business, and the vetting is meant to establish that this business is real and reachable. A description written around the platform vets the wrong company entirely, so the review proves nothing about the sender — and it happens by default, because the platform is the one filling in the form and its own name is the one it has to hand.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.description",
      "severity": "BLOCKING",
      "detectability": [
        "AI_FORM"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "Twilio",
        "AWS"
      ],
      "codes": [
        {
          "provider": "Twilio",
          "code": "30886",
          "remediable": true
        },
        {
          "provider": "Twilio",
          "code": "30894",
          "remediable": true
        }
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Write the description in the end customer's voice: their business name as sender, their customers as recipients, their opt-in surface as the source. Your platform belongs on the CSP record and nowhere in this field. Done when the description names the same business as the brand record.",
      "example": "Riverside Dental texts patients who booked online about upcoming appointments — not \"Northstar sends appointment reminders on behalf of dental practices\".",
      "pitfalls": [
        "\"Sent via Northstar\" reads as a disclosure and is still the platform in the field. Naming the platform anywhere in the description invites the reviewer to vet it instead."
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-038",
      "slug": "cmp-038",
      "title": "Campaign fields must reference one company, not several",
      "statement": "No campaign field may reference a second company, subsidiary, franchise or client brand alongside the registered one.",
      "rationale": "One campaign carries one brand's traffic, and a reviewer reading two company names cannot tell which one holds the consent or which one a complaint would be attributed to. Groups run into this honestly — the parent is the legal entity, the trading name is on the website, and a third name is on the app — so the description names all three in an effort to be complete and reads as pooled traffic.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.description + campaign.message_flow + campaign.sample[]",
      "severity": "BLOCKING",
      "detectability": [
        "AI_FORM"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "AWS",
        "Twilio"
      ],
      "codes": [
        {
          "provider": "Twilio",
          "code": "30926",
          "remediable": true
        }
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Pick the entity whose consent record backs this traffic and write every field in that name alone, registering a separate campaign for each other entity. Where the programme genuinely spans franchisees, AGENTS_FRANCHISES is the use case that lets sub-entities be disclosed rather than concealed.",
      "example": "Every field names Acme Coffee — not \"Acme Coffee, part of the Northstar group, on behalf of Acme Roasting Co\".",
      "pitfalls": [
        "The legal suffix and the trading name are one company: \"Acme Coffee Co, LLC\" and \"Acme Coffee\" are not two brands, and neither is a location suffix."
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-040",
      "slug": "cmp-040",
      "title": "Promotional content anywhere requires MARKETING to be declared",
      "statement": "Where any campaign text describes promotional or marketing content, MARKETING must be a declared use case or sub-use case.",
      "rationale": "The use case sets the consent standard the campaign is held to, and promotional content needs express written consent where transactional content does not — so an undeclared marketing programme is being judged against a standard that does not apply to it. Businesses reach this by classifying on intent rather than content: a shipping notification carrying a discount code is filed as a delivery notification because that is what it is for, and it is promotional the moment the code is in it.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.description + message_flow + sample[] ↔ campaign.usecase",
      "severity": "BLOCKING",
      "detectability": [
        "AI_FORM"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "Bandwidth",
        "Infobip",
        "Telnyx"
      ],
      "codes": [
        {
          "provider": "Bandwidth",
          "code": "3200",
          "remediable": true
        },
        {
          "provider": "Bandwidth",
          "code": "2200",
          "remediable": true
        },
        {
          "provider": "Bandwidth",
          "code": "8200",
          "remediable": true
        }
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Either declare MARKETING (or MIXED with a marketing sub-use case) and collect promotional-grade consent for everyone on the list, or take the promotional content out of every field until nothing offers, discounts or advertises. Done when the declared use case and the content agree in both directions.",
      "example": "Samples mention \"20% off all espresso blends this weekend\" → use case MARKETING, not DELIVERY_NOTIFICATION.",
      "pitfalls": [
        "Declaring MARKETING is the cheaper half of the fix; the expensive half is that every number on the list now needs express written consent, and re-labelling the campaign does not re-collect it.",
        "One promotional sentence inside an otherwise transactional message makes the whole message promotional. The reviewer grades the message, not the ratio."
      ],
      "notes": "One rule for four catalog rows because the assertion and the fix are identical wherever the promotional language turns up — description (CMP-040), CTA (CMP-071), samples (MSG-150) and the published privacy policy (POL-077). The providers issue a different reject code per field, which is why the catalog keeps them apart; a submitter fixing one has to fix all of them, so splitting the rule would report one defect four times.",
      "catalogIds": [
        "CMP-071",
        "MSG-150",
        "POL-077"
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-041",
      "slug": "cmp-041",
      "title": "The description must say how often the messages come",
      "statement": "The campaign description must disclose the messaging frequency.",
      "rationale": "Frequency is what a reviewer compares against the declared volume and against the frequency promised at opt-in, and a description that omits it leaves both comparisons unmade. It is also the disclosure a consumer relies on most directly: the difference between four messages a month and four a week is the difference between a programme they keep and one they report.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.description",
      "severity": "HIGH",
      "detectability": [
        "AI_FORM"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "Bandwidth"
      ],
      "codes": [
        {
          "provider": "Bandwidth",
          "code": "6004",
          "remediable": true
        }
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Add the frequency claim you make at the opt-in, in the same words. Done when the description, the CTA and the disclosure on your form all say the same thing about how often messages arrive.",
      "example": "Acme Coffee sends up to 4 promotional messages per month to customers who opted in at checkout.",
      "effectiveFrom": "2024-11-06",
      "notes": "Kept separate from CMP-073, which requires the same disclosure in the message-flow field: both fields carry it on the same registration, so satisfying one does not discharge the other. Effective from the Bandwidth vetting change of 2024-11-06.",
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-042",
      "slug": "cmp-042",
      "title": "A programme that asks for money must say so",
      "statement": "Where the campaign solicits donations, the description must state that donations are collected.",
      "rationale": "Donation solicitation is promotional content whatever the sender's tax status, and it carries fundraising rules of its own — so a charity programme described as \"updates for supporters\" is registered under the wrong consent standard. Non-profits are the ones this catches, and they are the least likely to see it coming, because internally the appeal is not thought of as marketing at all.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.description",
      "severity": "HIGH",
      "detectability": [
        "AI_FORM"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "Bandwidth",
        "Infobip",
        "Telnyx"
      ],
      "codes": [
        {
          "provider": "Bandwidth",
          "code": "3015",
          "remediable": true
        },
        {
          "provider": "Infobip",
          "code": "9003",
          "remediable": true
        }
      ],
      "applicability": {
        "useCases": [
          "CHARITY",
          "POLITICAL"
        ]
      },
      "applicabilityText": "Applies when the use case is CHARITY and POLITICAL.",
      "universal": false,
      "remediation": "Say plainly in the description that the programme asks supporters for donations, and confirm the opt-in disclosure said so too. Done when a reader knows before reading the samples that money will be requested.",
      "example": "Riverside Shelter texts supporters about shelter capacity and urgent supply needs, and asks for donations during winter appeals.",
      "notes": "Tagged to the two use cases whose programmes are fundraising by construction. A commercial programme that also solicits donations is reached by CMP-040 instead, since a donation ask is promotional content.",
      "catalogIds": [
        "MSG-270"
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-045",
      "slug": "cmp-045",
      "title": "The programme must be application-to-person, not personal messaging",
      "statement": "A campaign must describe application-to-person business messaging, not person-to-person or influencer-to-follower messaging.",
      "rationale": "10DLC exists for business messaging with consent behind it. A description reading as a person texting their contacts, or a public figure broadcasting to followers, does not fit the framework — and influencer programmes in particular usually have no per-number consent record at all, because the audience was acquired as followers rather than as subscribers.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.description + campaign.message_flow",
      "severity": "BLOCKING",
      "detectability": [
        "AI_FORM"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "Twilio",
        "AWS"
      ],
      "applicability": {
        "excludeUseCases": [
          "SOCIAL"
        ]
      },
      "applicabilityText": "Applies when the use case is NOT SOCIAL.",
      "universal": false,
      "remediation": "Describe the programme as business messaging from the registered entity to consumers who opted in with that entity, and say where those consumers opted in. Done when the description names the business as sender and an opt-in surface as the source of the list. If the programme genuinely is a public figure messaging followers, the SOCIAL use case is the route, not a reworded description.",
      "example": "Acme Coffee sends order updates and promotions to customers who opted in at checkout — not \"I text my followers about new drops\".",
      "notes": "Catalog CMP-046 flags the tension with the TCR SOCIAL use case, which Vonage restricts to exactly public figures, platforms and influencers (see CMP-140). Tagged to exclude SOCIAL so the rule does not reject the one use case built for it. SOCIAL is not in USE_CASE_REQUIREMENTS yet, so today the tag documents the carve-out rather than firing.",
      "catalogIds": [
        "CMP-046"
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-048",
      "slug": "cmp-048",
      "title": "Emergency framing requires the emergency use case",
      "statement": "A campaign may not describe itself as sending emergency or public-safety alerts unless EMERGENCY is declared and genuinely applies.",
      "rationale": "The emergency use case buys priority treatment and relaxed filtering, so describing ordinary traffic as urgent is an attempt — usually unwitting — to claim deliverability the programme is not entitled to. Businesses reach it through vocabulary rather than intent: an outage notice or a weather closure feels like an emergency, and the word costs the campaign its approval.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.description ↔ campaign.usecase",
      "severity": "HIGH",
      "detectability": [
        "AI_FORM"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "Twilio",
        "AWS"
      ],
      "codes": [
        {
          "provider": "Twilio",
          "code": "30929",
          "remediable": true
        }
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Describe the messages in operational terms — \"service interruption notices\", \"closure announcements\" — and reserve emergency language for the declared EMERGENCY use case, which is restricted to genuine public-safety senders. Done when no field claims urgency the use case does not carry.",
      "example": "Acme Coffee sends store closure notices during severe weather — not \"Acme Coffee sends emergency alerts to customers\".",
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-050",
      "slug": "cmp-050",
      "title": "The description must not contain personal data",
      "statement": "The campaign description must not contain individual names, account numbers or other personal data.",
      "rationale": "The description is stored and read across several organisations — the CSP, the registry, the carriers — and none of them is the right custodian for a customer's name or account number. It gets there through a well-meant example: someone illustrates the programme with a real message they sent, and the illustration carries a real person in it.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.description",
      "severity": "MEDIUM",
      "detectability": [
        "AI_FORM"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "Twilio"
      ],
      "codes": [
        {
          "provider": "Twilio",
          "code": "30886",
          "remediable": true
        }
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Replace any real person, account number or order reference with a category or a bracketed placeholder. Done when nothing in the field identifies an individual.",
      "example": "Acme Coffee texts customers their order status — not \"we text customers like Maria Alvarez about order 4471 on card ending 8891\".",
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-051",
      "slug": "cmp-051",
      "title": "A first-party lender must use the phrase the reviewer looks for",
      "statement": "A campaign whose business performs first-party lending must contain the literal phrase \"Direct Lending\" in its description.",
      "rationale": "Lending campaigns are screened against a prohibited category that looks almost identical from the outside — the difference between a licensed lender servicing its own loans and a lead-generation operation is not visible in a sample message. The literal phrase is the marker the screening looks for, so a compliant lender that describes itself in its own words is sorted into the refused pile and given a rejection that reads as a content ban.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.description",
      "severity": "HIGH",
      "detectability": [
        "AI_FORM"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "Twilio"
      ],
      "codes": [
        {
          "provider": "Twilio",
          "code": "30895",
          "remediable": true
        }
      ],
      "applicability": {
        "attributes": {
          "directLending": true
        }
      },
      "applicabilityText": "Applies when `directLending` is true.",
      "universal": false,
      "remediation": "Put the words \"Direct Lending\" in the description alongside your own account of the programme, and keep the directLending attribute true. Done when the phrase appears verbatim rather than as \"we lend directly\".",
      "example": "Harbor Point Lending is a Direct Lending business texting its own borrowers about payment due dates on loans it originated and services itself.",
      "pitfalls": [
        "The attribute and the phrase are separate requirements. Setting directLending true without the phrase leaves the screening with nothing to match on."
      ],
      "notes": "A Twilio phrasing requirement layered on top of the attribute itself. Not provider-tagged: including the phrase costs a compliant lender nothing anywhere else, and a tag would resolve YES on an unknown submission target regardless, so the tag would buy no protection while spending one of the three the registry allows itself.",
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-052",
      "slug": "cmp-052",
      "title": "A website that is not live yet must be declared as such",
      "statement": "Where the brand website is not yet published, the description must say so explicitly.",
      "rationale": "A reviewer who opens the submitted website and finds a parked domain or a 404 reads it as a brand that does not exist, which is the single fastest route to refusal. Saying \"the site launches in March; the opt-in form is at this staging URL with a screenshot attached\" turns the same facts into a documented pre-launch registration, and pre-launch registrations are routinely approved.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.description + brand.website",
      "severity": "HIGH",
      "detectability": [
        "AI_FORM"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "Twilio",
        "AWS"
      ],
      "codes": [
        {
          "provider": "Twilio",
          "code": "30891",
          "remediable": true
        }
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "State in the description that the site is not yet live, give the launch timing, and point at the evidence standing in for it. Done when a reviewer who finds nothing at the domain has already been told why.",
      "example": "Acme Coffee launches acmecoffee.com in March 2026. Until then the checkout opt-in runs on the staging site, screenshot attached: https://cdn.acmecoffee.com/compliance/optin.png",
      "catalogIds": [
        "CON-014",
        "WEB-149"
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-053",
      "slug": "cmp-053",
      "title": "A site a US reviewer cannot reach must be declared as such",
      "statement": "Where the website is geo-restricted so a US reviewer cannot open it, the description must disclose that and attach a screenshot.",
      "rationale": "A reviewer blocked at the door sees the same thing as a reviewer looking at a dead domain, and treats it the same way. The businesses this hits are the ones doing something reasonable — a regional retailer that serves one country and blocks the rest — so the rejection arrives for a deliberate business decision that nobody thought to mention.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.description + brand.website",
      "severity": "MEDIUM",
      "detectability": [
        "AI_FORM"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "Twilio"
      ],
      "codes": [
        {
          "provider": "Twilio",
          "code": "30891",
          "remediable": true
        }
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Say in the description which regions the site serves and that US traffic is blocked, and attach a screenshot of the opt-in page taken from inside the served region. Done when the reviewer has an image of what they cannot load.",
      "example": "The Acme Coffee storefront serves Canada only and blocks US traffic; the checkout opt-in is captured here: https://cdn.acmecoffee.com/compliance/optin-ca.png",
      "catalogIds": [
        "CON-015"
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-056",
      "slug": "cmp-056",
      "title": "An ISV registering for a customer must supply the customer's details",
      "statement": "Where an ISV or platform registers on behalf of a customer, the campaign must carry the customer's website and brand information, not the platform's.",
      "rationale": "This is the campaign-side twin of registering the wrong brand. The platform's own site is real and substantial, which makes it a tempting field value — and it means the entity being vetted is not the business whose messages will be sent, so the vetting proves nothing about the sender.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign fields + brand.website",
      "severity": "BLOCKING",
      "detectability": [
        "AI_FORM"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "AWS"
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Populate every campaign field from the end customer: their website, their brand name, their opt-in surface. Your platform belongs on the CSP record only. Done when nothing in the campaign identifies the platform.",
      "example": "Website: acmecoffee.com (the customer) — not northstarplatform.io (the ISV).",
      "notes": "Catalog CMP-056 is conditional on the registration being on behalf of a third party. We hold no \"registered by an ISV\" fact, so the condition lives in the judgement criteria instead of the applicability tag — the rule only fires a finding when the content itself shows a platform.",
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-057",
      "slug": "cmp-057",
      "title": "A platform registering its own product must say that it is",
      "statement": "Where the registrant is a platform registering a direct offering of its own, the description must state that explicitly.",
      "rationale": "Platforms are screened hard for registering their customers' traffic under their own brand, so a platform that genuinely is the sender looks exactly like the thing reviewers are trained to refuse. Saying so directly is what separates the two, and it is the one thing the description of a legitimate platform programme usually omits — because from the inside there is nothing ambiguous about it.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.description",
      "severity": "HIGH",
      "detectability": [
        "AI_FORM"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "AWS"
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "State that the messages are your own product's notifications to your own users, and name the product. Done when the description rules out the reading that you are sending on a customer's behalf.",
      "example": "Northstar texts its own subscribing practices about billing and platform status — these are Northstar's notifications to Northstar customers, not messages sent for a practice.",
      "notes": "The mirror of CMP-056, which catches a platform registering a customer's traffic under its own brand. Conditional on the registrant being a platform at all, and we hold no such fact, so the condition lives in the criteria: the rule opens with a PASS boundary and only speaks when the content itself shows a platform.",
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-058",
      "slug": "cmp-058",
      "title": "Some programmes are better served by another channel",
      "statement": "A campaign whose traffic does not need 10DLC should be told that toll-free or another channel fits it better.",
      "rationale": "10DLC carries per-campaign vetting, registration fees and throughput tiers that a low-volume one-way notification programme gains nothing from, and a business that registers anyway pays for review cycles it did not need. This is advice rather than a defect, and it is worth giving early: the cost of hearing it after three rejections is much higher than the cost of hearing it now.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.description + campaign.usecase",
      "severity": "LOW",
      "detectability": [
        "AI_FORM"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "AWS"
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Weigh toll-free verification against 10DLC for a low-volume, one-way, non-marketing programme — it is a single verification rather than a per-campaign one. Done when the channel is a decision you made rather than a default.",
      "example": "A clinic sending a few hundred appointment reminders a month is usually better served by a verified toll-free number than by a registered 10DLC campaign.",
      "pitfalls": [
        "Toll-free is not an escape from a content rejection: the same prohibited categories are refused there, so a campaign turned down for its content will be turned down again."
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-059",
      "slug": "cmp-059",
      "title": "Message flow must describe the mechanism end to end",
      "statement": "The message-flow / CTA field must state the exact consent-capture mechanism end to end.",
      "rationale": "Naming a channel is not describing a mechanism. A reviewer needs the whole path — where the consumer is, what they see, what they tick or say, what happens next — because each step is where a compliance defect hides, and a description that skips steps is indistinguishable from one that has steps to hide.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.message_flow",
      "severity": "BLOCKING",
      "detectability": [
        "AI_FORM"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "TCR",
        "Twilio",
        "Bandwidth",
        "AWS"
      ],
      "applicability": {
        "excludeUseCases": [
          "M2M"
        ]
      },
      "applicabilityText": "Applies when the use case is NOT M2M.",
      "universal": false,
      "remediation": "Write the flow as a single narrative covering place, audience, prompt, action, disclosure wording, and what is stored. Done when the paragraph reads as a walkthrough someone could follow. Put it in campaign.message_flow itself — this field is graded on its own text, not on what a reviewer would find by opening your site.",
      "example": "Customers opt in at https://acmecoffee.com/checkout, in the block beneath the delivery address fields, after entering delivery details and before placing the order. They see an unchecked box labelled \"Text me offers\" with this disclosure printed beside it: \"By checking this box you agree to receive recurring automated marketing text messages from Acme Coffee at the number provided. Consent is not a condition of any purchase. Message frequency varies, up to 4 messages per month. Message and data rates may apply. Reply STOP to unsubscribe or HELP for help.\" Only customers who tick the box are subscribed; they receive order and delivery updates plus weekly promotional offers. Each opt-in is stored with the phone number, timestamp, the disclosure version shown, and the checkout session id. Screenshot: https://cdn.acmecoffee.com/compliance/optin.png",
      "notes": "Catalog CMP-059 is universal except use_case=M2M, which has no consumer to consent. M2M is not in USE_CASE_REQUIREMENTS yet, so today the tag documents the carve-out rather than firing.",
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-060",
      "slug": "cmp-060",
      "title": "Every opt-in method in use must be listed in the message flow",
      "statement": "All opt-in methods actually in use must be described inside the single message-flow field.",
      "rationale": "There is one flow field and it has to cover every collection surface, because an undescribed method is an unaudited one. Brands routinely describe only their web form while also collecting consent verbally at a counter, which leaves the larger half of their list undocumented — and it is the undocumented half that generates the complaints.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.message_flow",
      "severity": "BLOCKING",
      "detectability": [
        "AI_FORM"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "TCR",
        "Twilio",
        "Bandwidth",
        "AWS"
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "List every method you actually use in campaign.message_flow, each in its own paragraph. If you collect consent three ways, the field describes three flows. Done when the methods described match the methods you could be audited on.",
      "example": "Consent is collected three ways: (1) the checkout box at acmecoffee.com/checkout [screenshot attached]; (2) verbally at our counter using the attached script; (3) by texting JOIN to 55512 from our in-store signage [photo attached].",
      "pitfalls": [
        "Dropping a method from the flow does not remove the obligation — it removes the evidence that you meet it."
      ],
      "catalogIds": [
        "CON-003",
        "CON-004"
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-061",
      "slug": "cmp-061",
      "title": "Each declared method needs its own complete workflow paragraph",
      "statement": "Every declared opt-in method must carry its own description satisfying that method's own rule set.",
      "rationale": "A compliant web form does not make a verbal opt-in compliant — the two have different required elements, different evidence, and different failure modes. One combined paragraph covering \"various channels\" satisfies neither rule set and is graded against both.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.message_flow",
      "severity": "BLOCKING",
      "detectability": [
        "AI_FORM"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "TCR",
        "Twilio"
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Give each method its own paragraph carrying that method's required elements: the web form needs the checkbox and disclosure, the verbal path needs the script elements, the keyword path needs the advertised CTA and confirmation. Done when each paragraph would pass on its own if the others were deleted.",
      "example": "Web: customers tick an unchecked box at acmecoffee.com/checkout reading \"…\". Verbal: agents read the attached script capturing an explicit yes. Keyword: in-store signage reads \"Text JOIN to 55512. Msg frequency varies…\".",
      "notes": "The per-method formulation is marked [INFERRED] in the research — derived from the TCR \"you must list them all\" instruction plus Twilio 30909 rejection causes, not published as a rule in those words. Cite the underlying obligation, not this phrasing, when a user pushes back. Applies only when more than one method is declared; the applicability model cannot express \"more than one\", so the judgement carries that condition instead.",
      "catalogIds": [
        "CON-005"
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-062",
      "slug": "cmp-062",
      "title": "Declared opt-in methods must all actually be used",
      "statement": "Opt-in methods selected on the registration must be methods the brand genuinely uses; unused selections must be deselected.",
      "rationale": "Every declared method is a promise a reviewer will check, and each one needs its own evidence. Ticking every box to look thorough multiplies the ways the campaign can fail, because a declared method with no matching evidence is a mismatch rather than a bonus.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign opt-in method declarations",
      "severity": "HIGH",
      "detectability": [
        "DETERMINISTIC"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "TCR",
        "Twilio",
        "Bandwidth",
        "AWS"
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Declare only the methods actually in use today. Remove aspirational ones — you can add them later when the evidence exists.",
      "example": "Declared: web form only. Not: web form + keyword + QR + verbal, with one screenshot for all four.",
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-063",
      "slug": "cmp-063",
      "title": "A method you use and did not declare is the one that will be found",
      "statement": "Every opt-in method actually in use must appear in the submission; none may be left out.",
      "rationale": "The undeclared method is the one generating complaints, because it is the one nobody applied the disclosure checklist to. Businesses omit them without deciding to: the web form is what the registration form asks about, and the fact that the sales team also collects numbers at trade shows never comes up until a recipient asks where the number came from.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "declared opt-in methods vs the flows actually in use",
      "severity": "BLOCKING",
      "detectability": [
        "HUMAN"
      ],
      "failureClass": "TERMINAL_EVIDENCE",
      "authorities": [
        "Twilio"
      ],
      "codes": [
        {
          "provider": "Twilio",
          "code": "30909",
          "remediable": true
        }
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Ask everyone who could add a number to the list how they do it — sales, support, retail, events, the website — and declare every route that turns up. Done when nobody in the business can name a way onto the list that is not in the submission.",
      "notes": "The mirror of CMP-062, and the half of the pair no payload can settle: a method absent from the submission is also absent from everything we can read. The user has to canvass their own team and add whatever surfaces; we cannot check the answer, so a PASS here means nobody has been asked rather than that nothing was omitted, and the product must say so when it reports one.",
      "phase": "approval",
      "automated": false,
      "attestation": {
        "question": "Has everyone who can add a number to this list been asked how they do it — and does every route they named appear in the submission?",
        "howToCheck": [
          "Canvass sales, support, retail, events and the website. The web form is what the registration form asks about; the trade-show clipboard never comes up.",
          "Add every route that surfaces to the declared methods.",
          "Done when nobody in the business can name a way onto the list that is not in the submission."
        ],
        "failureLooksLike": "The undeclared method is the one generating complaints, because it is the one nobody applied the disclosure checklist to. A pass here means nobody has been asked, not that nothing was omitted."
      }
    },
    {
      "id": "CMP-064",
      "slug": "cmp-064",
      "title": "The opt-in page URL must be a deep link to the consent surface",
      "statement": "The URL in the message flow must point at the page that carries the consent surface, verified against the crawl.",
      "rationale": "A reviewer given a URL expects to land on the consent form. Landing on a homepage, a 404, or a page whose form is elsewhere converts a verifiable opt-in into an unverifiable claim, which is the most common reason an otherwise compliant flow is rejected.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.message_flow + website",
      "severity": "BLOCKING",
      "detectability": [
        "AI_FORM"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "TCR",
        "Twilio",
        "Bandwidth",
        "AWS"
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Link the exact page holding the consent checkbox, and open it in a private window to confirm it loads for someone with no session.",
      "example": "https://acmecoffee.com/signup — the page with the checkbox, not the homepage.",
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-065",
      "slug": "cmp-065",
      "title": "A hosted artifact is required when the opt-in is not publicly crawlable",
      "statement": "Where the opt-in surface is login-gated, unpublished, on paper, verbal, or in-store, a publicly hosted screenshot URL is required in the message flow.",
      "rationale": "A reviewer cannot crawl what is not on the public web, so the artifact becomes the only evidence that exists. This is the rule that makes offline collection registrable at all — without it those campaigns simply fail the website-consent checks for reasons that do not apply to them.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.message_flow + consent artifact",
      "severity": "BLOCKING",
      "detectability": [
        "DETERMINISTIC"
      ],
      "failureClass": "TERMINAL_EVIDENCE",
      "authorities": [
        "TCR",
        "Twilio",
        "Bandwidth",
        "AWS"
      ],
      "applicability": {
        "consentMethods": [
          "verbal_live",
          "verbal_ivr",
          "paper",
          "pos",
          "employer_hr",
          "account_settings"
        ]
      },
      "applicabilityText": "Applies when consent was collected by live verbal, IVR, paper form, point of sale, employer / HR and account settings.",
      "universal": false,
      "remediation": "Host the evidence at a public URL that opens without a login and cite it in the message flow. Confirm it opens in a private browsing window before submitting.",
      "example": "Consent screenshot: https://cdn.acmecoffee.com/compliance/counter-form.pdf",
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-067",
      "slug": "cmp-067",
      "title": "The hosted evidence URL must return the artifact, not an error page",
      "statement": "A hosted evidence URL must return 200 and render the artifact rather than a 404, a timeout, a redirect or a placeholder.",
      "rationale": "Evidence links rot between writing the registration and the review, and the failure is silent from the submitter's side because their browser serves the file from cache. A reviewer who follows the link and sees a 404 does not chase it — the campaign is rejected for missing evidence that the submitter can still see on their own screen.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "hosted evidence URL",
      "severity": "BLOCKING",
      "detectability": [
        "CRAWL"
      ],
      "failureClass": "TERMINAL_EVIDENCE",
      "authorities": [
        "Twilio",
        "Aerialink"
      ],
      "codes": [
        {
          "provider": "Twilio",
          "code": "30891",
          "remediable": true
        },
        {
          "provider": "Twilio",
          "code": "30922",
          "remediable": true
        }
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Open every evidence URL in a private browsing window on a device that has never seen it, and replace any that do not render the file itself. Done when each link returns the image or PDF directly rather than a viewer page, a login, or a \"file not found\".",
      "pitfalls": [
        "A cloud-drive preview page returns 200 while showing the reviewer a sign-in prompt, so status code alone is not the test — what came back has to be the artifact."
      ],
      "catalogIds": [
        "WEB-105"
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-068",
      "slug": "cmp-068",
      "title": "Evidence links must keep working after approval",
      "statement": "Hosted evidence URLs must stay reachable for the life of the campaign, not only until it is approved.",
      "rationale": "The evidence is what you rely on when a complaint arrives, which is typically long after approval — and by then the share link has expired, the CDN path has been reorganised, or the file was cleaned up as part of a migration. Approval is the point at which everyone stops thinking about these URLs, which is precisely why they rot.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "hosted evidence URL",
      "severity": "HIGH",
      "detectability": [
        "CRAWL"
      ],
      "failureClass": "TERMINAL_EVIDENCE",
      "authorities": [
        "Twilio"
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Host evidence on a path you control with no expiry, and re-check the links whenever the campaign is renewed or resubmitted. Done when the URLs are somewhere your own retention policy protects rather than in a share link.",
      "pitfalls": [
        "A cloud-drive share link is the common failure: many expire on a schedule, and revoking one person's access can silently break it for everyone."
      ],
      "notes": "Partly off-platform: we can fetch the URLs at scan time, but nobody can verify at registration that they will still resolve in a year. The user has to put the files somewhere durable and re-check them at renewal — the research marks the ongoing-monitoring half as an inference rather than a published rule, so it is surfaced as a warning with the check the user should run themselves, not as a finding against the submission.",
      "catalogIds": [
        "CON-026",
        "WEB-156",
        "OPS-271"
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-069",
      "slug": "cmp-069",
      "title": "A URL mentioned in the CTA must also be given as a link field",
      "statement": "Any URL referenced inside the message-flow text must also be supplied in the structured link fields.",
      "rationale": "The structured fields are what gets crawled and screened; a URL that exists only inside prose is read by a human and checked by nothing. Submitters reasonably assume that writing the link once is enough, and the result is a privacy policy that was never fetched, never screened, and never confirmed to exist.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.message_flow ↔ campaign.privacy_url + terms_url",
      "severity": "HIGH",
      "detectability": [
        "AI_FORM"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "Bandwidth"
      ],
      "codes": [
        {
          "provider": "Bandwidth",
          "code": "2120",
          "remediable": true
        }
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Copy each URL out of the flow text into the field built for it — privacy policy, terms, opt-in page — leaving it in the prose as well. Done when every link in the flow also appears in a structured field.",
      "example": "The flow mentions https://acmecoffee.com/privacy → the privacy policy field also carries https://acmecoffee.com/privacy.",
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-072",
      "slug": "cmp-072",
      "title": "Message flow must state the message types",
      "statement": "The message-flow / CTA field must state the message types the subscriber will receive.",
      "rationale": "Consent is scoped to what was disclosed, so the flow has to record what the consumer was told they would get. A flow that describes the mechanics of ticking a box without saying what arrives afterwards documents an opt-in to nothing in particular, and leaves a reviewer no way to check the samples against the consent.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.message_flow",
      "severity": "BLOCKING",
      "detectability": [
        "AI_FORM"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "TCR",
        "Twilio",
        "Bandwidth",
        "AWS"
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Name the categories you actually send — \"order and delivery updates plus weekly promotional offers\" — instead of \"messages\". Done when a reader can predict, from the flow alone, what will land on the subscriber's phone. Put it in campaign.message_flow itself — this field is graded on its own text, not on what a reviewer would find by opening your site.",
      "example": "Subscribers receive order and delivery updates plus weekly promotional offers from Acme Coffee.",
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-073",
      "slug": "cmp-073",
      "title": "Message flow must state message frequency",
      "statement": "The message-flow / CTA field must state the message frequency.",
      "rationale": "Frequency is one of the required disclosures at the point of consent, so its absence from the flow text tells a reviewer either that it was never disclosed or that the flow is incomplete. Both readings cost the submission, and the second one costs it twice because the reviewer then doubts the rest of the description.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.message_flow",
      "severity": "BLOCKING",
      "detectability": [
        "AI_FORM"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "TCR",
        "Twilio",
        "Bandwidth",
        "AWS"
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Add the frequency claim you make at the opt-in, word for word. Done when the flow and the disclosure on your form say the same thing — a reviewer comparing them will notice if one says \"varies\" and the other promises four a month. Put it in campaign.message_flow itself — this field is graded on its own text, not on what a reviewer would find by opening your site.",
      "example": "Message frequency varies, up to 4 messages per month.",
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-074",
      "slug": "cmp-074",
      "title": "Message flow must state that rates may apply",
      "statement": "The message-flow / CTA field must state that message and data rates may apply.",
      "rationale": "The consumer may be billed per message by their own carrier, and this is the disclosure that tells them so before they agree. It is also the most mechanically-checked element in the framework — reviewers look for it in the flow text as well as on the surface — so omitting it is a certain finding in exchange for eight saved words.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.message_flow",
      "severity": "BLOCKING",
      "detectability": [
        "AI_FORM"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "TCR",
        "Twilio",
        "Bandwidth",
        "AWS"
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Quote the rates sentence inside the disclosure you reproduce in the flow. Done when the phrase \"Message and data rates may apply\" (or a clear equivalent) appears in the flow text itself. Put it in campaign.message_flow itself — this field is graded on its own text, not on what a reviewer would find by opening your site.",
      "example": "The disclosure beside the box reads \"Message and data rates may apply.\"",
      "notes": "The catalog exempts Free-To-End-User programmes here (CTIA/T-Mobile, see CMP-152). We hold no FTEU fact, so the applicability model cannot express the carve-out and the rule fires universally; an FTEU programme will see a finding it can dismiss.",
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-075",
      "slug": "cmp-075",
      "title": "Message flow must state how to get help",
      "statement": "The message-flow / CTA field must state how the consumer gets help.",
      "rationale": "The HELP route is part of the disclosure the consumer sees at opt-in, so it belongs in the record of what they were shown. It also lets a reviewer confirm that the flow, the configured help keywords, and the HELP auto-reply all describe the same programme — three fields that drift apart easily because three different people fill them in.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.message_flow",
      "severity": "BLOCKING",
      "detectability": [
        "AI_FORM"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "TCR",
        "Twilio",
        "Bandwidth",
        "AWS"
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Include the HELP instruction inside the quoted disclosure, and name where HELP lands. Done when the flow names the same keyword you configured on the campaign. Put it in campaign.message_flow itself — this field is graded on its own text, not on what a reviewer would find by opening your site.",
      "example": "The disclosure beside the box reads \"Reply HELP for help\"; HELP is answered by our support desk at support@acmecoffee.com.",
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-076",
      "slug": "cmp-076",
      "title": "Message flow must state how to opt out",
      "statement": "The message-flow / CTA field must state how the consumer opts out.",
      "rationale": "The opt-out instruction is the single most important disclosure a consumer is given, and a flow that omits it is read as evidence that the surface omits it too. Reviewers treat the flow text as the record of what the consumer actually saw, so silence here is not neutral — it is a claim that nothing was said.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.message_flow",
      "severity": "BLOCKING",
      "detectability": [
        "AI_FORM"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "TCR",
        "Twilio",
        "Bandwidth",
        "AWS"
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Include the STOP instruction inside the quoted disclosure. Done when the word STOP appears in the flow text and matches the opt-out keywords declared on the campaign. Put it in campaign.message_flow itself — this field is graded on its own text, not on what a reviewer would find by opening your site.",
      "example": "The disclosure beside the box reads \"Reply STOP to unsubscribe\"; STOP is honoured immediately and confirmed.",
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-077",
      "slug": "cmp-077",
      "title": "Message flow must carry the terms",
      "statement": "The message-flow / CTA field must state the programme terms, either in full or as a link.",
      "rationale": "The terms are what the consumer is agreeing to, so a flow that describes an agreement without saying where its content lives records a consent to something unstated. Reviewers treat the flow as the record of what was shown, and a missing terms reference reads as terms that were never presented — which is also usually what happened, because the link sits in the page footer rather than beside the box.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.message_flow",
      "severity": "BLOCKING",
      "detectability": [
        "AI_FORM"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "Bandwidth",
        "Telnyx",
        "TCR"
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Put the terms URL inside the disclosure you reproduce in the flow, or write the terms out in full there. Done when the flow text itself contains either the wording or a link a reviewer can open. Put it in campaign.message_flow itself — this field is graded on its own text, not on what a reviewer would find by opening your site.",
      "example": "The disclosure beside the box links the programme terms at https://acmecoffee.com/sms-terms.",
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-078",
      "slug": "cmp-078",
      "title": "Message flow must carry the privacy commitment",
      "statement": "The message-flow / CTA field must state the privacy policy link, or that opt-in data will not be shared.",
      "rationale": "The single commitment carriers care most about is that a mobile number collected for messaging is not passed on, and this is where the consumer is told it. Either form satisfies it — the link or the sentence — which is worth knowing, because a business with no separate privacy page can discharge the requirement with one clause rather than by publishing a document.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.message_flow",
      "severity": "BLOCKING",
      "detectability": [
        "AI_FORM"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "Bandwidth",
        "Telnyx"
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Add the privacy policy URL to the disclosure you reproduce in the flow, or state in the flow that mobile opt-in data is never shared with third parties. Done when one of the two appears in the flow text. Put it in campaign.message_flow itself — this field is graded on its own text, not on what a reviewer would find by opening your site.",
      "example": "The disclosure beside the box reads \"We never share your mobile information with third parties\" and links https://acmecoffee.com/privacy.",
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-079",
      "slug": "cmp-079",
      "title": "Message flow must describe the consumer-facing opt-in action",
      "statement": "The message-flow / CTA field must state how the consumer is told about the programme and what they do to join.",
      "rationale": "The flow must describe the experience, not the plumbing: what the consumer sees, and the specific action they take. This is what lets a reviewer decide the action was affirmative rather than passive — the difference between a box someone ticked and a form someone submitted while consent was assumed.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.message_flow",
      "severity": "BLOCKING",
      "detectability": [
        "AI_FORM"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "TCR",
        "Twilio",
        "Bandwidth",
        "AWS"
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Describe the control the consumer sees, its label, and the action they take on it. Done when the text names a deliberate act — ticking, texting, signing, saying yes. Put it in campaign.message_flow itself — this field is graded on its own text, not on what a reviewer would find by opening your site.",
      "example": "Beneath the delivery fields the customer sees an unchecked box labelled \"Text me offers\" with the disclosure printed beside it, and ticks it before placing the order.",
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-080",
      "slug": "cmp-080",
      "title": "The disclosure must be reproduced in the registration, not only on the site",
      "statement": "The CTA disclosure text must appear inside the registration itself even when it is also published on the website.",
      "rationale": "A reviewer grades the registration record, and a website can change between submission and review — so the copy in the flow field is what the approval is actually granted against. This is the rule behind a surprising share of rejections on sites that are perfectly compliant: everything the reviewer needed was on the page, and none of it was in the form.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.message_flow",
      "severity": "HIGH",
      "detectability": [
        "AI_FORM"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "Aerialink",
        "TCR"
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Paste the disclosure the consumer sees into the flow field verbatim, in quotation marks, rather than linking to it. Done when the flow contains the sentence itself and not a pointer to where the sentence lives.",
      "example": "The box is labelled \"Text me offers\" and the disclosure printed beside it reads: \"By checking this box you agree to receive recurring automated marketing text messages from Acme Coffee at the number provided. Consent is not a condition of any purchase. Message frequency varies. Message and data rates may apply. Reply STOP to unsubscribe or HELP for help.\"",
      "notes": "Verbatim from the source: \"Even though this information is available on the website, it must be displayed within TCR.\"",
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-082",
      "slug": "cmp-082",
      "title": "Message flow must state where the opt-in occurs",
      "statement": "The message-flow / CTA field must state where on the site, app, or premises the opt-in happens.",
      "rationale": "Reviewers verify by going and looking, so the flow must tell them where to go, described from the consumer's point of view rather than in internal system terms. \"On the checkout page after entering delivery details\" is findable; \"via our CDP integration\" is not, and an unfindable opt-in is treated as an unverifiable one.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.message_flow",
      "severity": "BLOCKING",
      "detectability": [
        "AI_FORM"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "TCR",
        "Twilio",
        "Bandwidth",
        "AWS"
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Give the URL or the physical place, plus the position on the page. Done when someone holding only the flow text could navigate to the control and see it. Put it in campaign.message_flow itself — this field is graded on its own text, not on what a reviewer would find by opening your site.",
      "example": "Opt-in happens on the checkout page at https://acmecoffee.com/checkout, in the block beneath the delivery address fields.",
      "catalogIds": [
        "CON-006"
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-084",
      "slug": "cmp-084",
      "title": "Message flow must state who opts in and when",
      "statement": "The message-flow / CTA field must state who is opting in and at what moment.",
      "rationale": "Which customers, at which point in their journey, is what determines whether the consent scope matches the messages sent. A flow that never says who is subscribing cannot be checked against the samples, and it is the field where list-purchase and scraped-number programmes give themselves away.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.message_flow",
      "severity": "BLOCKING",
      "detectability": [
        "AI_FORM"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "TCR",
        "Twilio",
        "Bandwidth",
        "AWS"
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Name the audience and the moment: which customers, at which step. Done when the flow makes clear that only people who took the action are on the list. Put it in campaign.message_flow itself — this field is graded on its own text, not on what a reviewer would find by opening your site.",
      "example": "Customers opt in during checkout, after entering delivery details and before placing the order; only customers who tick the box are subscribed.",
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-085",
      "slug": "cmp-085",
      "title": "Message flow must state how consent is recorded",
      "statement": "The message-flow / CTA field must state how consent is recorded once given.",
      "rationale": "The record is what you rely on when a complaint arrives, and reviewers read its absence as evidence that nothing is retained. Storing only \"opted in: true\" leaves you unable to show WHAT was agreed to, which is the question every TCPA complaint turns on.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.message_flow",
      "severity": "BLOCKING",
      "detectability": [
        "AI_FORM"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "TCR",
        "Twilio",
        "Bandwidth",
        "AWS"
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "State the fields you keep: number, timestamp, the disclosure version shown, and the session or agent identity. Done when the flow shows you could reproduce what a given subscriber saw on the day they agreed. Put it in campaign.message_flow itself — this field is graded on its own text, not on what a reviewer would find by opening your site.",
      "example": "Each opt-in is stored with the phone number, timestamp, the disclosure version shown, and the checkout session id.",
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-088",
      "slug": "cmp-088",
      "title": "The opt-in path should follow a published flow template",
      "statement": "The described opt-in should follow one of the published message-flow templates: digital, verbal, paper, or inbound keyword.",
      "rationale": "Reviewers work from four templates and check the described flow against whichever one it resembles, so a flow that matches none of them is assessed against a standard nobody has written down — which in practice means the strictest reading of all four. Matching a template is not bureaucratic conformity: it is what makes the review predictable, and it usually only requires reordering what is already there.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.message_flow",
      "severity": "BLOCKING",
      "detectability": [
        "AI_FORM"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "Telnyx"
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Restructure the flow to follow the template for your method: digital gives page, control, disclosure, storage; verbal gives context, script, capture, retention; paper gives form, consent language, signature, storage; keyword gives advertisement, keyword, number, confirmation. Done when a reader can name which template the paragraph follows.",
      "example": "Digital template, in order: customers reach https://acmecoffee.com/checkout · tick an unchecked box labelled \"Text me offers\" · read the disclosure printed beside it · the opt-in is stored with number, timestamp and disclosure version.",
      "notes": "Published by one provider as a set of four templates rather than as a rule everyone states. Kept universal and graded as guidance in the criteria: a flow carrying every required element in an unusual order is not a defect, so the FAIL boundary is drawn at a flow that matches no template because elements are missing rather than because they are ordered differently.",
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-089",
      "slug": "cmp-089",
      "title": "Some opt-in must be described somewhere",
      "statement": "The description or the message flow must describe some form of opt-in, however briefly.",
      "rationale": "This is the floor beneath every other consent rule: a submission that never mentions how anyone joined the list is indistinguishable from one where nobody did. It is a different failure from a thin description — the elements are not merely incomplete, the subject is absent — and it is fixed by writing a sentence rather than by improving one.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.description + campaign.message_flow",
      "severity": "BLOCKING",
      "detectability": [
        "AI_FORM"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "Bandwidth"
      ],
      "codes": [
        {
          "provider": "Bandwidth",
          "code": "2100",
          "remediable": true
        }
      ],
      "applicability": {
        "excludeUseCases": [
          "M2M"
        ]
      },
      "applicabilityText": "Applies when the use case is NOT M2M.",
      "universal": false,
      "remediation": "Write one sentence saying where consumers agree to receive the messages, then expand it into a full flow. Done when a reader can answer \"how did these people end up on this list?\" from the submission alone.",
      "example": "Customers opt in at checkout on acmecoffee.com by ticking a box to receive offers by text.",
      "notes": "The presence floor beneath CMP-059's quality bar. Kept separate because they fail and are fixed differently: CMP-059 grades a described mechanism against a standard, while this one asks whether a mechanism was described at all, and a submission failing this one fails CMP-059 for a reason that would mislead.",
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-091",
      "slug": "cmp-091",
      "title": "Implied consent needs a conversation the consumer started",
      "statement": "Implied consent is acceptable only where both the use case and the message flow are conversational, with the consumer sending first.",
      "rationale": "Implied consent exists for the case where a consumer texts a business and the business texts back, because the consumer initiating is the consent. Stretching it to cover a list the business assembled inverts that: nobody initiated anything, and the \"conversation\" is one-way. Businesses land here by reasoning from an existing relationship — these are our customers, they gave us their number — which is exactly the inference the standard exists to block.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.usecase + campaign.message_flow",
      "severity": "BLOCKING",
      "detectability": [
        "AI_FORM"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "CTIA",
        "Infobip"
      ],
      "codes": [
        {
          "provider": "Infobip",
          "code": "3013A",
          "remediable": true
        }
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Collect express consent for anything the consumer did not start: a checkbox, a keyword they texted, a form they submitted. Reserve implied consent for replies inside a thread the consumer opened. Done when every number on the list either texted you first or affirmatively opted in.",
      "example": "A customer texts Acme Coffee asking about store hours and we reply about store hours — that is the whole of what implied consent covers.",
      "pitfalls": [
        "A number collected during a purchase is not a consumer-initiated conversation; the purchase was the conversation, and it was not by text."
      ],
      "notes": "Conditional on implied consent actually being claimed, and we hold no such fact, so the condition lives in the criteria and the rule opens with a PASS boundary. CTIA is explicit that a one-way alert is never conversational however short the messages are.",
      "catalogIds": [
        "CON-119"
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-092",
      "slug": "cmp-092",
      "title": "Consent grade must match content grade",
      "statement": "The consent standard evidenced must match the content grade: conversational needs implied consent, informational needs express consent, promotional needs express written consent.",
      "rationale": "Consent comes in grades and content comes in grades, and the content grade sets the floor. A promotional programme resting on an implied consent collected for a service purpose is the most common structural consent failure — it looks like consent exists, and it is the wrong kind, so the brand cannot understand the rejection and re-submits the same evidence.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.message_flow + campaign.usecase",
      "severity": "BLOCKING",
      "detectability": [
        "AI_FORM"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "CTIA",
        "FCC",
        "T-Mobile",
        "AT&T"
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Raise the consent to match the content: promotional messaging needs express written consent — an affirmative, recorded agreement naming the sender and the message type. Re-collect it if what you hold was gathered for a service purpose; you cannot re-characterise consent after the fact. Done when the flow describes a written, affirmative act for every subscriber who will receive promotional content.",
      "example": "Promotional consent is collected in writing at checkout: the customer ticks an unchecked box beside the disclosure naming Acme Coffee and the message types, and we store the number, timestamp, and disclosure version.",
      "pitfalls": [
        "Splitting the campaign into a promotional one and a transactional one does not launder the consent — the promotional half still needs express written consent for each number on it."
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-094",
      "slug": "cmp-094",
      "title": "Each declared opt-in method needs its own evidence",
      "statement": "A screenshot or artifact is required for EACH opt-in method declared, not one artifact covering the set.",
      "rationale": "Methods differ structurally — a web checkbox, a paper form and a phone script share no visual elements — so one artifact cannot evidence several. Reviewers check them individually, and the missing ones are read as undocumented collection surfaces.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "consent artifacts + declared methods",
      "severity": "HIGH",
      "detectability": [
        "DETERMINISTIC"
      ],
      "failureClass": "TERMINAL_EVIDENCE",
      "authorities": [
        "TCR",
        "Twilio",
        "Bandwidth",
        "AWS"
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Attach one artifact per declared method: a screenshot for the web form, a scan for the paper form, the script for phone consent, a capture for the QR landing page.",
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-095",
      "slug": "cmp-095",
      "title": "A web opt-in must supply the form itself, not just describe it",
      "statement": "A web-form or checkout opt-in must supply the link to the opt-in page, or a screenshot of the form.",
      "rationale": "A reviewer confirms a web opt-in by looking at it, and a description with nothing to look at is an assertion rather than evidence. This is the most common shape of an incomplete submission because the flow text reads as complete — every element is described, and there is simply no way to check that any of it is on the page.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.message_flow + opt-in evidence",
      "severity": "BLOCKING",
      "detectability": [
        "DETERMINISTIC"
      ],
      "failureClass": "TERMINAL_EVIDENCE",
      "authorities": [
        "Bandwidth",
        "Twilio"
      ],
      "codes": [
        {
          "provider": "Twilio",
          "code": "30896",
          "remediable": true
        }
      ],
      "applicability": {
        "consentMethods": [
          "web_form",
          "checkout"
        ]
      },
      "applicabilityText": "Applies when consent was collected by web form and checkout.",
      "universal": false,
      "remediation": "Put the deep link to the page carrying the consent control in the message flow, and attach a screenshot of that control taken on a fresh page load. Done when someone with no account can open the link and see the box you described.",
      "pitfalls": [
        "A link to the homepage is not a link to the opt-in form. If the control is three clicks in, the screenshot is doing the work and the link is not enough on its own."
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-096",
      "slug": "cmp-096",
      "title": "Keyword opt-in requires both the keywords and the confirmation text",
      "statement": "A keyword-based opt-in must populate both the opt-in keywords and the opt-in confirmation message.",
      "rationale": "The keyword is how the consumer joins and the confirmation is what tells them they succeeded; a keyword programme with no confirmation leaves them texting into silence, then texting again. Providers check the pair together because either alone is an incomplete programme.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.optin_keywords + campaign.optin_message",
      "severity": "BLOCKING",
      "detectability": [
        "DETERMINISTIC"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "TCR",
        "Twilio",
        "Bandwidth",
        "AWS"
      ],
      "applicability": {
        "consentMethods": [
          "keyword"
        ]
      },
      "applicabilityText": "Applies when consent was collected by text-to-join keyword.",
      "universal": false,
      "remediation": "Populate campaign.optin_keywords with the keyword your signage advertises, and write the confirmation message carrying brand, frequency, rates, HELP and STOP. Done when texting the keyword would produce that exact reply.",
      "example": "optin_keywords: JOIN, START · optin_message: \"Acme Coffee: you're subscribed to offers. Msg frequency varies. Msg & data rates may apply. Reply HELP for help, STOP to cancel.\"",
      "pitfalls": [
        "The keyword in the campaign must match the keyword on the signage exactly — JOIN and JOINUS are different programmes to a reviewer comparing the two."
      ],
      "catalogIds": [
        "CMP-196"
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-097",
      "slug": "cmp-097",
      "title": "A keyword opt-in must give the keyword and the number to text it to",
      "statement": "A keyword opt-in must state the exact keyword and the exact destination number in the message flow.",
      "rationale": "A reviewer verifies a keyword programme by texting the keyword to the number and seeing what comes back, and a flow missing either half makes that impossible. Both halves are usually known to everyone involved and written down nowhere, because the signage carries them and the form does not ask for them in so many words.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.message_flow",
      "severity": "BLOCKING",
      "detectability": [
        "AI_FORM"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "Aerialink",
        "Telnyx"
      ],
      "codes": [
        {
          "provider": "Twilio",
          "code": "30909",
          "remediable": true
        }
      ],
      "applicability": {
        "consentMethods": [
          "keyword"
        ]
      },
      "applicabilityText": "Applies when consent was collected by text-to-join keyword.",
      "universal": false,
      "remediation": "Write the keyword and the destination number into the flow exactly as they appear on your signage, including the short code or long code digits. Done when someone holding only the flow text could join the programme from their own phone.",
      "example": "Supporters text SHELTER to 55512, as printed on the shelter posters.",
      "pitfalls": [
        "The keyword in the flow, the keyword on the signage and the keyword configured on the campaign have to be the same word — JOIN and JOINUS are different programmes to a reviewer comparing them."
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-100",
      "slug": "cmp-100",
      "title": "The declared opt-in methods must match the flow described",
      "statement": "The opt-in methods ticked on the form must be the ones the message flow describes, in both directions.",
      "rationale": "The structured list is what routes the submission to the right evidence checks, and the narrative is what a human reads; when they disagree the campaign is graded against a rule set that does not fit it. A flow describing a QR poster with only \"web form\" ticked will be checked for a web opt-in page that does not exist, and the rejection names the missing page rather than the missing tick.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.opt_in_methods[] + campaign.message_flow",
      "severity": "BLOCKING",
      "detectability": [
        "AI_FORM"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "Twilio",
        "Telnyx"
      ],
      "codes": [
        {
          "provider": "Twilio",
          "code": "30909",
          "remediable": true
        }
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Tick exactly the methods your flow describes — MOBILE_QR_CODE for a scanned code, PAPER_FORM for a signed sheet, and so on — and delete any tick with no paragraph behind it. Done when each ticked method has a paragraph and each paragraph has a tick.",
      "example": "The flow describes a poster QR and a checkout box → opt-in methods MOBILE_QR_CODE and WEB_FORM, and nothing else.",
      "pitfalls": [
        "Fixing the mismatch by deleting the paragraph rather than ticking the box hides a collection surface you are still using, which is the more serious of the two failures."
      ],
      "notes": "One rule for two catalog rows: CMP-100 requires MOBILE_QR_CODE to be declared for a QR flow and CMP-106 requires PAPER_FORM for a paper one, which is a single enum-versus-narrative consistency check with the value changing. Deliberately not tagged to those two consent methods — the mismatch is worth catching whichever method is involved, and tagging it would exclude precisely the registrations whose ticks are wrong.",
      "catalogIds": [
        "CMP-106"
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-101",
      "slug": "cmp-101",
      "title": "A QR opt-in must supply the QR code image itself",
      "statement": "A QR-code opt-in must include the QR image in the registration evidence.",
      "rationale": "The QR code is the only part of a QR flow a reviewer can independently verify: they scan it and see where it actually goes, which is the one thing the description cannot establish. A code that resolves somewhere other than the stated destination is the specific defect this evidence exists to catch, and it is invisible in every other field.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "opt-in evidence (OptInImageURLs)",
      "severity": "BLOCKING",
      "detectability": [
        "DETERMINISTIC"
      ],
      "failureClass": "TERMINAL_EVIDENCE",
      "authorities": [
        "Twilio"
      ],
      "codes": [
        {
          "provider": "Twilio",
          "code": "30896",
          "remediable": true
        }
      ],
      "applicability": {
        "consentMethods": [
          "qr"
        ]
      },
      "applicabilityText": "Applies when consent was collected by QR code.",
      "universal": false,
      "remediation": "Attach the QR image exactly as it appears on the printed piece — the same code, not a regenerated one pointing at the same URL — and scan it yourself from a printout before submitting. Done when scanning the attached image lands on the opt-in destination the flow describes.",
      "pitfalls": [
        "Regenerating a \"clean\" QR for the submission defeats the check: the reviewer is verifying the code that is actually in the wild, and a code that has since been re-pointed will scan differently from the one on your poster."
      ],
      "notes": "The presence half is settled here; whether the scanned destination is a compliant opt-in surface is CON-QR-DEST and CMP-103, which read the destination rather than the file. Split because they fail differently and are fixed by different people — one by attaching a file, the other by rebuilding a landing page.",
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-103",
      "slug": "cmp-103",
      "title": "A QR flow must say where the code leads",
      "statement": "A QR opt-in must state whether the code opens a hosted opt-in form or a pre-filled SMS in the native messaging app.",
      "rationale": "Both destinations are permitted and they are graded by completely different rules — a hosted form owes a checkbox and a disclosure, a pre-filled SMS owes a confirmation reply and keyword handling — so a reviewer who cannot tell which one this is cannot begin. Submitters omit it because from their side there is only one obvious answer, and which one is obvious depends on who built the poster.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.message_flow + QR destination",
      "severity": "BLOCKING",
      "detectability": [
        "AI_FORM"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "Twilio",
        "Klaviyo"
      ],
      "codes": [
        {
          "provider": "Twilio",
          "code": "30909",
          "remediable": true
        }
      ],
      "applicability": {
        "consentMethods": [
          "qr"
        ]
      },
      "applicabilityText": "Applies when consent was collected by QR code.",
      "universal": false,
      "remediation": "Say which of the two the code does, and give the destination: the opt-in URL, or the number and body of the pre-filled message. Done when the flow names the destination type in words rather than leaving it to be inferred from the URL.",
      "example": "Scanning the poster QR opens a pre-filled SMS to 55512 with the body JOIN; the customer presses send, which is the opt-in.",
      "notes": "Neither destination is preferred — the rule is about the flow saying which one, not about choosing between them. Whether a hosted destination is a specific opt-in page rather than the homepage is CMP-064, which reads the URL itself.",
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-112",
      "slug": "cmp-112",
      "title": "A promotional programme cannot rest on spoken consent alone",
      "statement": "A campaign whose content requires express written consent must not declare a spoken opt-in as its only collection method.",
      "rationale": "Written consent exists so that what the consumer agreed to can be produced years later when a complaint arrives, and a phone call produces no such record — an agent's note that the customer said yes cannot show what they were told they were saying yes to. Businesses reach this honestly: the call centre already asks for consent for service messages, and adding the marketing programme to the same script looks like a small extension rather than a change of consent grade.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.usecase + declared consent methods",
      "severity": "BLOCKING",
      "detectability": [
        "DETERMINISTIC"
      ],
      "failureClass": "TERMINAL_ARTIFACT",
      "artifact": "paper_consent_form",
      "authorities": [
        "FCC",
        "Twilio",
        "Infobip",
        "Bandwidth"
      ],
      "codes": [
        {
          "provider": "Twilio",
          "code": "30913",
          "remediable": true
        },
        {
          "provider": "Bandwidth",
          "code": "2101",
          "remediable": true
        }
      ],
      "applicability": {
        "consentMethods": [
          "verbal_live",
          "verbal_ivr"
        ]
      },
      "applicabilityText": "Applies when consent was collected by live verbal and IVR.",
      "universal": false,
      "remediation": "Collect the promotional consent in writing — a signed form at the counter, or a web opt-in the customer completes themselves — and keep the spoken path for the service messages it is valid for. Done when every number receiving promotional content has a written record behind it.",
      "pitfalls": [
        "Numbers already collected verbally cannot be moved onto the written programme by re-characterising the consent; they have to be asked again.",
        "Reading the written disclosure aloud does not make the consent written. What matters is the record the consumer produced, not the words the agent said."
      ],
      "notes": "The use-case condition lives in the check rather than in an applicability tag, because it is read off USE_CASE_REQUIREMENTS.consentGrade — a table row rather than a fixed list, so a use case added later is covered without editing this rule. The tag narrows to the spoken methods, which is what makes the rule inapplicable to a web or checkout opt-in instead of passing trivially on one. The legal layer records the Fifth Circuit *Bradley v. DentalPlans.com* carve-out; carriers do not honour it, so the registry does not either.",
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-113",
      "slug": "cmp-113",
      "title": "Debt collection cannot rest on a spoken opt-in",
      "statement": "A debt-collection campaign must not collect its consent verbally.",
      "rationale": "Debt collection is already the most heavily litigated messaging category, and a spoken consent produces no record of what the debtor was told — so the one category where the consent record is most likely to be examined is the one where it would be weakest. The overlap with third-party collection matters too: that is refused outright at most providers, so a verbal opt-in here often signals a programme that has a larger problem than its consent method.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.message_flow + brand.vertical",
      "severity": "BLOCKING",
      "detectability": [
        "AI_FORM"
      ],
      "failureClass": "TERMINAL_ARTIFACT",
      "artifact": "paper_consent_form",
      "authorities": [
        "Aerialink"
      ],
      "applicability": {
        "verticals": [
          "debt_collection"
        ]
      },
      "applicabilityText": "Applies when the vertical is debt_collection.",
      "universal": false,
      "remediation": "Collect written consent for the debt-collection programme — a signed form at account opening, or a portal opt-in the debtor completes themselves — and keep a copy of the disclosure version each debtor saw. Done when every number on the programme has a written record behind it.",
      "pitfalls": [
        "Consent obtained by the original creditor does not transfer to a third-party collector, so re-collecting is not optional even where a written record exists upstream."
      ],
      "notes": "Narrower than CMP-112, which refuses spoken consent for promotional content generally: this one refuses it for a whole vertical regardless of use case, because the exposure comes from the subject matter rather than from the content grade. Third-party debt collection is separately prohibited outright by the MSG-DEBT prohibition, which fires on content rather than on vertical.",
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-115",
      "slug": "cmp-115",
      "title": "A point-of-sale opt-in must show the sign the customer read",
      "statement": "A point-of-sale opt-in must supply a hosted photo of the signage, kiosk or tablet screen carrying the disclosure.",
      "rationale": "An in-store opt-in leaves nothing on the web to inspect, so the photograph is the entire evidentiary record of what the customer was shown before they gave their number. Counter signage is also where disclosures are most often abbreviated for space — the frequency and rates lines are the first to go — which is exactly what the photo is there to reveal.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "hosted evidence URL",
      "severity": "HIGH",
      "detectability": [
        "VISION"
      ],
      "failureClass": "TERMINAL_EVIDENCE",
      "authorities": [
        "CTIA",
        "Twilio",
        "TextUs",
        "HighLevel"
      ],
      "applicability": {
        "consentMethods": [
          "pos"
        ]
      },
      "applicabilityText": "Applies when consent was collected by point of sale.",
      "universal": false,
      "remediation": "Photograph the sign or the tablet screen where it stands, close enough to read every line of the disclosure, and host it at a public URL cited in the flow. Done when the disclosure text is legible in the image without zooming.",
      "pitfalls": [
        "A photograph of the artwork file is not a photograph of the sign. Reviewers are checking that the disclosure is actually displayed at the point of collection, not that it was designed."
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-116",
      "slug": "cmp-116",
      "title": "An email-activated opt-in must show the email that asked",
      "statement": "Where the opt-in is solicited by email, a screenshot of that email must be supplied.",
      "rationale": "The email is where the consumer was told what they were joining, so it carries the disclosures the web form usually would. Businesses treat it as marketing collateral rather than as consent evidence and often no longer have the exact version that was sent, which is why the screenshot is asked for at registration rather than at complaint time.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "opt-in evidence",
      "severity": "HIGH",
      "detectability": [
        "VISION"
      ],
      "failureClass": "TERMINAL_EVIDENCE",
      "authorities": [
        "Bandwidth"
      ],
      "applicability": {
        "consentMethods": [
          "email"
        ]
      },
      "applicabilityText": "Applies when consent was collected by email.",
      "universal": false,
      "remediation": "Send yourself the live activation email, screenshot it whole — subject line, body, and the link or button that starts the opt-in — and host it. Done when the image shows what the recipient saw, not a template preview.",
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-118",
      "slug": "cmp-118",
      "title": "A pop-up opt-in must be declared as one",
      "statement": "Where the opt-in is presented in a pop-up or modal, the message flow must say so.",
      "rationale": "A reviewer opening the page sees no consent control, because the modal fires on a timer, on exit intent, or only for new visitors — so a perfectly compliant pop-up opt-in looks like a page with no opt-in on it. Saying \"the opt-in appears in a modal after a few seconds on first visit\" is the difference between an approval and a rejection for a form that was there the whole time.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.message_flow",
      "severity": "HIGH",
      "detectability": [
        "AI_FORM"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "Telnyx"
      ],
      "applicability": {
        "consentMethods": [
          "web_form",
          "checkout"
        ]
      },
      "applicabilityText": "Applies when consent was collected by web form and checkout.",
      "universal": false,
      "remediation": "State that the opt-in is a pop-up, what triggers it, and attach a screenshot of it open. Done when a reviewer who sees no form on the page has already been told why and has an image of it.",
      "example": "The opt-in appears in a pop-up shown to first-time visitors after 5 seconds on https://acmecoffee.com; screenshot attached: https://cdn.acmecoffee.com/compliance/popup-optin.png",
      "notes": "Tagged to the web methods because a modal is a web presentation; a spoken or paper opt-in cannot have one. The rule asks only that the presentation be declared — whether a pop-up is an acceptable surface at all is CON-DISC-NO-POPUP, which grades the disclosure inside it.",
      "catalogIds": [
        "CON-050"
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-119",
      "slug": "cmp-119",
      "title": "Consent collected on a third-party platform must still name the registered brand",
      "statement": "Consent evidence captured on a third-party platform must show the registered brand as the party the consumer agreed to hear from.",
      "rationale": "A consumer who opts in inside a marketplace, a social platform or a chat widget is agreeing to hear from whoever is named on that screen. When the screen names only the platform, the consent belongs to the platform and not to the brand sending the messages — which is the consent-transfer problem the framework exists to prevent, arrived at by accident rather than by design.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "opt-in evidence captured on a third-party surface",
      "severity": "BLOCKING",
      "detectability": [
        "VISION"
      ],
      "failureClass": "TERMINAL_EVIDENCE",
      "authorities": [
        "Twilio"
      ],
      "codes": [
        {
          "provider": "Twilio",
          "code": "30927",
          "remediable": true
        }
      ],
      "applicability": {
        "consentMethods": [
          "third_party",
          "social",
          "chat"
        ]
      },
      "applicabilityText": "Applies when consent was collected by third party, social and chat.",
      "universal": false,
      "remediation": "Configure the third-party surface to name your business in the consent text — most let you set the merchant or sender name — and re-capture the screenshot afterwards. Done when the registered brand name is visible in the same frame as the consent control.",
      "pitfalls": [
        "The platform's own branding in the header does not substitute for naming your business in the consent sentence, and a reviewer reads the sentence rather than the chrome."
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-122",
      "slug": "cmp-122",
      "title": "The opt-in screenshot must show the terms and privacy links",
      "statement": "A screenshot submitted as opt-in evidence must show the Terms and Privacy Policy links as the consumer sees them.",
      "rationale": "The links being present on the page is not the question — the question is whether they were visible at the moment of consent, and only the screenshot answers it. A footer link three screens below the form is on the page and was not part of the decision, which is the distinction reviewers are looking for when they ask for this framing.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "opt-in screenshot",
      "severity": "HIGH",
      "detectability": [
        "VISION"
      ],
      "failureClass": "TERMINAL_EVIDENCE",
      "authorities": [
        "LeadConnector",
        "Twilio"
      ],
      "codes": [
        {
          "provider": "Twilio",
          "code": "30909",
          "remediable": true
        }
      ],
      "applicability": {
        "consentMethods": [
          "web_form",
          "checkout"
        ]
      },
      "applicabilityText": "Applies when consent was collected by web form and checkout.",
      "universal": false,
      "remediation": "Re-frame the screenshot to include the consent control and the terms and privacy links in one image. If they do not fit in one frame, move the links up beside the control rather than stitching two screenshots together.",
      "pitfalls": [
        "Stitching a crop of the form to a crop of the footer proves the opposite of what it is meant to: it shows the two were not visible together."
      ],
      "catalogIds": [
        "POL-036"
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-124",
      "slug": "cmp-124",
      "title": "A multi-step consent flow must be captured at every step",
      "statement": "Where consent is collected across several screens, evidence must show each step rather than the final one.",
      "rationale": "In a multi-step flow the disclosure and the consent control are usually on different screens, so a screenshot of the step carrying the button proves nothing about what was disclosed before it. This is the flow shape where a compliant programme most often looks non-compliant — everything required was shown, and the evidence only covers the last screen of it.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "opt-in screenshots",
      "severity": "HIGH",
      "detectability": [
        "VISION"
      ],
      "failureClass": "TERMINAL_EVIDENCE",
      "authorities": [
        "Twilio",
        "LeadConnector",
        "optinfix"
      ],
      "codes": [
        {
          "provider": "Twilio",
          "code": "30917",
          "remediable": true
        }
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Capture every screen between arriving and consenting, in order, and combine them into one PDF. Done when the sequence shows the disclosure and the consent act without a gap a reviewer has to fill in.",
      "example": "A three-page PDF: the product page with the offer, the form with the phone field and disclosure, the confirmation screen after submitting.",
      "catalogIds": [
        "CON-032"
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-127",
      "slug": "cmp-127",
      "title": "A consent surface a reviewer cannot reach must be declared as such",
      "statement": "Where the opt-in sits behind a login, is unpublished, is spoken, or is on paper, the submission must say so explicitly.",
      "rationale": "A reviewer who cannot find the opt-in assumes it does not exist, because that is far more often true than the alternative. Saying \"this opt-in is inside the customer account portal, screenshot attached\" converts a dead end into a documented one — the same registration, differing only in whether the reviewer had to guess.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.message_flow",
      "severity": "HIGH",
      "detectability": [
        "AI_FORM"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "AWS",
        "Twilio"
      ],
      "codes": [
        {
          "provider": "Twilio",
          "code": "30909",
          "remediable": true
        }
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Add one sentence naming why the surface is not reachable and what stands in for it. Done when the flow text answers the question a reviewer would otherwise have to ask.",
      "example": "The opt-in toggle sits inside the authenticated account portal and cannot be crawled, so a screenshot taken from a live account is attached: https://cdn.acmecoffee.com/compliance/portal-optin.png",
      "catalogIds": [
        "CON-021"
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-128",
      "slug": "cmp-128",
      "title": "An evidence field with nothing to say should be left empty",
      "statement": "Evidence fields that do not apply must be omitted rather than filled with filler text.",
      "rationale": "Filler in an evidence field converts a question a reviewer would have skipped into one they now have to resolve, and \"N/A\" beside a field they expected content in reads as a refusal rather than an absence. It comes from forms that mark every field required, so the submitter types something to get past validation and inherits a finding.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "evidence fields",
      "severity": "LOW",
      "detectability": [
        "AI_FORM"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "Twilio"
      ],
      "codes": [
        {
          "provider": "Twilio",
          "code": "30917",
          "remediable": true
        }
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Clear any field holding \"N/A\", \"none\", \"see above\" or a repeat of another field. Where a form will not accept an empty value, say in one clause why the field does not apply. Done when every populated field carries information.",
      "example": "A campaign with no paper opt-in leaves the paper-form evidence field empty — not \"N/A - we do not use paper forms, see web form above\".",
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-129",
      "slug": "cmp-129",
      "title": "Evidence must come from one company, not several",
      "statement": "A campaign's opt-in evidence must all come from the registered brand, not from subsidiaries, franchises or client brands.",
      "rationale": "A bundle showing three businesses tells a reviewer that consent was pooled across them, and pooled consent is not transferable to whichever entity happens to be sending. Agencies and franchisors hit this honestly by attaching the best examples they have rather than the ones belonging to the registrant, and the submission is refused for a reason that reads as pedantic until you notice it is the whole question.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "opt-in evidence across the bundle",
      "severity": "BLOCKING",
      "detectability": [
        "AI_FORM"
      ],
      "failureClass": "TERMINAL_EVIDENCE",
      "authorities": [
        "Twilio"
      ],
      "codes": [
        {
          "provider": "Twilio",
          "code": "30926",
          "remediable": true
        }
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Attach only artifacts captured from this brand's own consent surfaces, and register a separate campaign for each other entity. Where the programme genuinely spans franchisees, the AGENTS_FRANCHISES use case is the route that lets sub-entities be disclosed rather than hidden.",
      "example": "Every screenshot shows acmecoffee.com — not one from acmecoffee.com and one from a sister brand that happens to run the same form.",
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-131",
      "slug": "cmp-131",
      "title": "Evidence must travel by the route the submission target actually offers",
      "statement": "Evidence must reach the reviewer through the channel the submission target provides, whether an upload field, a TCR attachment, or a URL inside the flow.",
      "rationale": "Every platform exposes exactly one evidence channel and none of them is the same: one has a multimedia upload field, one attaches the form to TCR, one accepts files only in its console and needs a URL in the payload when you use the API. Evidence lodged in the wrong place is not late or malformed — it is invisible, and the rejection reads as though nothing was ever attached.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "evidence channel + campaign.message_flow",
      "severity": "HIGH",
      "detectability": [
        "DETERMINISTIC"
      ],
      "failureClass": "TERMINAL_EVIDENCE",
      "authorities": [
        "Bandwidth",
        "Telnyx",
        "Bird"
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Use your target's channel: Bandwidth's \"CTA, Privacy Policy and/or Terms and Conditions Multimedia Upload\" field; a TCR attachment on the express-consent form for Telnyx; Bird's console attachments, or the proof URL inside the messageFlow JSON when you submit by API because attachments there are UI-only. Done when the evidence is reachable from the submission itself rather than from your own console.",
      "example": "Submitting to Bird by API: messageFlow ends with \"Opt-in proof: https://cdn.acmecoffee.com/compliance/optin.png\" because attachments cannot be sent over the API.",
      "pitfalls": [
        "Attaching in the console and submitting by API is the combination that silently loses the evidence: the file is visible to you and absent from the payload the reviewer receives."
      ],
      "notes": "Three catalog rows, one obligation with the destination as its parameter — Bandwidth's upload field (CMP-131), Telnyx's TCR attachment (CMP-133) and Bird's API workaround (CMP-134). Written as one rule because a submitter has exactly one target and needs exactly one instruction; three rules would report the same gap three times with two of them irrelevant. We can see that evidence exists and whether the flow text carries a URL, but not which UI field a file was uploaded through, so the check settles the observable half and the remediation carries the rest.",
      "catalogIds": [
        "CMP-133",
        "CMP-134"
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-132",
      "slug": "cmp-132",
      "title": "Where the platform has no upload field, proof must be a URL in the flow text",
      "statement": "On a platform offering no evidence upload, every artifact must be reachable from a public URL written into the message-flow text.",
      "rationale": "Some platforms surface no file-upload field anywhere in campaign registration, so evidence attached in your own console never reaches the reviewer — they read the message-flow text and nothing else. The submission then looks complete from your side and arrives empty at theirs, which is why the rejection reason is \"no opt-in evidence\" on a campaign whose evidence you can see.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.message_flow + hosted evidence URLs",
      "severity": "BLOCKING",
      "detectability": [
        "DETERMINISTIC"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "Twilio"
      ],
      "codes": [
        {
          "provider": "Twilio",
          "code": "30909",
          "remediable": true
        },
        {
          "provider": "Twilio",
          "code": "30921",
          "remediable": true
        }
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Host each artifact at a URL that opens without a login and paste those URLs into the message-flow text itself, labelled with what each one shows. Done when the flow text alone would let a reviewer reach every piece of evidence.",
      "example": "Screenshot of the checkout consent box: https://cdn.acmecoffee.com/compliance/optin.png · Verbal script: https://cdn.acmecoffee.com/compliance/script.pdf",
      "notes": "The research marks the \"no upload path exists\" finding as an inference drawn from uniform Twilio wording across eight rejection codes rather than from a published statement, so the provider condition is applied in the check and the rule passes on any other target. Cite the underlying obligation — evidence must reach the reviewer — rather than this phrasing if a user pushes back.",
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-139",
      "slug": "cmp-139",
      "title": "Each declared sub-use case needs its own sample message",
      "statement": "A campaign declaring sub-use cases must carry at least one sample message for each of them.",
      "rationale": "A MIXED campaign is several programmes sharing one registration, and a reviewer approves each of them on the strength of a sample. Declaring five sub-use cases and supplying two samples asks them to approve three programmes they have not seen, so they refuse — and the rejection names the sample count, which sends the submitter to add generic samples rather than to the sub-use case that had nothing behind it.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.sample[] + campaign.sub_use_cases[]",
      "severity": "BLOCKING",
      "detectability": [
        "DETERMINISTIC"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "Bandwidth",
        "Telnyx",
        "AWS",
        "Sinch",
        "Bird",
        "Vonage"
      ],
      "codes": [
        {
          "provider": "TCR",
          "code": "8102",
          "remediable": true
        }
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Write one sample per declared sub-use case, each showing that sub-use case and nothing else. Done when you can point at the sample that justifies every sub-use case you ticked — and if you cannot write one, untick it.",
      "example": "Sub-use cases MARKETING + DELIVERY_NOTIFICATION → sample1 \"Acme Coffee: 20% off all beans this weekend. Reply STOP to opt out.\" and sample2 \"Acme Coffee: your order #4471 ships today. Reply STOP to opt out.\"",
      "pitfalls": [
        "Reusing one promotional sample for two promotional sub-use cases does not satisfy this — the reviewer is counting distinct programmes, not distinct wordings."
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-141",
      "slug": "cmp-141",
      "title": "Sample count must meet the minimum for the declared use case",
      "statement": "The campaign must carry at least the number of sample messages its use case requires.",
      "rationale": "Sample minimums vary by use case, and special use cases generally require more because reviewers need to see the breadth of a programme that gets elevated treatment. A campaign that satisfies the global minimum can still fail its own use case.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.sample[] + campaign.usecase",
      "severity": "BLOCKING",
      "detectability": [
        "DETERMINISTIC"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "TCR",
        "Twilio",
        "Bandwidth",
        "AWS"
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Add sample messages until the use-case minimum is met, each showing a genuinely different scenario rather than a reworded duplicate.",
      "example": "MARKETING requires at least 2 distinct samples.",
      "catalogIds": [
        "CMP-136"
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-142",
      "slug": "cmp-142",
      "title": "Use case must be available to the brand entity type",
      "statement": "Restricted use cases must only be selected by the entity types eligible for them.",
      "rationale": "Several use cases are gated on entity type — charity to non-profits, sole-proprietor to individuals, emergency to registered organisations. Selecting one the brand cannot hold produces a rejection that reads as a campaign problem while the real fix is on the brand.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.usecase + brand.entity_type",
      "severity": "BLOCKING",
      "detectability": [
        "DETERMINISTIC"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "TCR",
        "Twilio",
        "Infobip"
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Either choose a use case your entity type can hold, or re-register the brand under the entity type the use case requires.",
      "example": "CHARITY requires entity type NON_PROFIT.",
      "catalogIds": [
        "BRD-159"
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-175",
      "slug": "cmp-175",
      "title": "Subscriber attributes must be true where the API accepts nothing else",
      "statement": "On a CSP whose API accepts only true for the subscriber opt-in, opt-out and help attributes, none of the three may be submitted false.",
      "rationale": "This is a schema rejection rather than a vetting one: the call fails before the campaign exists, so there is no rejection reason to read and no campaign record to inspect. It bites exactly the submissions that are entitled to say false — a two-factor programme relying on the opt-out exemption TCR grants finds that its provider never implemented the exemption.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.subscriber_optin + subscriber_optout + subscriber_help",
      "severity": "BLOCKING",
      "detectability": [
        "DETERMINISTIC"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "Plivo"
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Send true for all three attributes on this provider, and back each one with real handling — declared keywords and a reply body — rather than leaning on the use-case exemption. Done when the payload carries three true values and three configured behaviours.",
      "example": "subscriber_optin: true · subscriber_optout: true · subscriber_help: true",
      "notes": "Plivo publishes \"Allowed value: true\" for all three fields. The provider condition is applied inside the check rather than as a `providers` tag: a tag resolves YES when the submission target is unknown, which would make this rule contradict CON-010's two-factor carve-out on every registration that has not chosen a provider yet. Passing on an unknown target is the honest reading — the exemption is real everywhere except here.",
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-176",
      "slug": "cmp-176",
      "title": "Declaring opt-in requires opt-in keywords where the provider pairs them",
      "statement": "Where the provider ties the two fields together, declaring subscriberOptin true requires optinKeywords to be populated.",
      "rationale": "The provider models opt-in as keyword-driven and validates the pair, so a web-form programme that has no keyword to give is rejected for a field it had no reason to fill in. The fix is not to invent a keyword but to declare the ones the programme already honours — START and its variants are answered by every platform whether or not anyone advertises them.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.optin_keywords + campaign.subscriber_optin",
      "severity": "BLOCKING",
      "detectability": [
        "DETERMINISTIC"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "Bird"
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Populate the opt-in keyword field with the keywords your platform actually answers — START and UNSTOP at minimum, plus any keyword your signage advertises. Done when the field is non-empty and every keyword in it triggers a subscription on your side.",
      "example": "subscriberOptin: true · optin_keywords: START, UNSTOP, JOIN",
      "notes": "Bird's campaign form makes optinKeywords conditionally mandatory on subscriberOptin. The condition is applied inside the check rather than as a `providers` tag, because a tag resolves YES on an unknown submission target and would then demand opt-in keywords from every web-form and checkout registration in the registry — TCR requires them only for keyword opt-in (CMP-096).",
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-187",
      "slug": "cmp-187",
      "title": "The wider opt-out keyword set is required on some platforms",
      "statement": "Where the platform publishes a wider standard opt-out set, the declared keywords must cover HALT and STOPALL as well as the universal five.",
      "rationale": "A consumer who texts HALT has opted out as far as they are concerned, and on a platform that treats HALT as a recognised opt-out the message is suppressed whether or not you declared it — so a declaration that omits it puts your suppression list out of step with what the platform has already done. You then keep sending to someone the platform believes unsubscribed, which is the pattern complaint volume is measured on.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.optout_keywords",
      "severity": "BLOCKING",
      "detectability": [
        "DETERMINISTIC"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "Twilio"
      ],
      "codes": [
        {
          "provider": "Twilio",
          "code": "30887",
          "remediable": true
        }
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Add HALT and STOPALL to the declared opt-out keywords, and confirm your platform suppresses on both. Done when the declared set covers STOP, STOPALL, UNSUBSCRIBE, CANCEL, END, QUIT, HALT, OPTOUT and REVOKE.",
      "example": "optout_keywords: STOP, STOPALL, UNSUBSCRIBE, CANCEL, END, QUIT, HALT, OPTOUT, REVOKE",
      "notes": "Twilio's published set adds HALT and STOPALL to the seven FCC per-se revocation keywords that CON-KW-REVOCATION-SET requires. Kept out of the universal baseline deliberately: neither the FCC nor CTIA lists them, so demanding them everywhere would fail compliant registrations to satisfy one platform. On an unknown submission target the rule passes and names the two additions rather than failing, because we cannot yet know whether they are owed. An incomplete or empty list is OPS-026 and OPS-027's finding, not this one.",
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-188",
      "slug": "cmp-188",
      "title": "A non-standard opt-out keyword is honoured by nobody but you",
      "statement": "Opt-out keywords outside the standard monitored set are handled by the sending platform alone and must not be relied on as carrier-enforced.",
      "rationale": "The standard keywords are intercepted upstream, so they work even when a platform is misconfigured; anything else works only for as long as your own handler does. A programme whose advertised opt-out word is bespoke has a single point of failure between the consumer and their opt-out, and the consumer who used it and kept receiving messages complains to the carrier rather than to you.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.optout_keywords",
      "severity": "MEDIUM",
      "detectability": [
        "DETERMINISTIC"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "Azure ACS",
        "AWS"
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Keep the bespoke keyword if your platform answers it, but add the standard words alongside so the opt-out survives a misconfiguration on your side. Done when every keyword you advertise for opting out is either standard or backed by one that is.",
      "example": "optout_keywords: STOP, END, CANCEL, UNSUBSCRIBE, QUIT — not STOPMSGS or NOMORE on their own",
      "notes": "Scoped to the opt-out list rather than to every keyword field, which is what the catalog row names. A non-standard HELP keyword costs a recipient an unanswered question; a non-standard opt-out keyword costs them an opt-out that never happened, and only the second is worth a finding. INFO and SUPPORT as help keywords are normal practice and are deliberately not reported here.",
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-AFFILIATE",
      "slug": "cmp-affiliate",
      "title": "Affiliate marketing campaigns are refused",
      "statement": "A campaign declaring affiliate marketing — sharing opt-ins with, or promoting, third parties — is rejected.",
      "rationale": "Affiliate marketing means messaging people whose consent someone else collected, which is the transfer of consent the entire framework prohibits. Declaring it is a self-reported disqualification, and no use case or disclosure makes it registrable.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.affiliate_marketing",
      "severity": "BLOCKING",
      "detectability": [
        "DETERMINISTIC"
      ],
      "failureClass": "HARD_STOP",
      "authorities": [
        "TCR",
        "Twilio",
        "T-Mobile",
        "CTIA"
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Affiliate and lead-generation messaging cannot be registered. Consent is non-transferable: only the business that collected it may message the consumer.",
      "catalogIds": [
        "CON-016"
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-AUTOREPLY-LEN",
      "slug": "cmp-autoreply-len",
      "title": "Auto-reply messages must be 20–320 characters",
      "statement": "The opt-in, opt-out and HELP reply messages must each be 20–320 characters.",
      "rationale": "Auto-replies must fit the carrier limits for a single message, and a reply truncated mid-sentence loses precisely the disclosure it exists to deliver. The floor of 20 characters exists because a two-word reply cannot carry the brand name and a contact route.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.help_message + optin_message + optout_message",
      "severity": "HIGH",
      "detectability": [
        "DETERMINISTIC"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "Twilio",
        "Bandwidth"
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Trim each auto-reply to fit within 320 characters while keeping the brand name, the STOP instruction and the rates notice — those are the elements other rules require. If it will not fit, drop the marketing language rather than the disclosures.",
      "example": "Acme Coffee: for help email support@acmecoffee.com. Reply STOP to unsubscribe.",
      "notes": "Bird and Sinch cap at 255; Twilio and Bandwidth at 320. The 320 ceiling is used with the tighter 255 recorded as a provider divergence.",
      "catalogIds": [
        "CMP-007",
        "CMP-008",
        "CMP-009"
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-DESC-LEN",
      "slug": "cmp-desc-len",
      "title": "Campaign description must be 40–4096 characters",
      "statement": "The campaign description must be at least 40 and at most 4096 characters.",
      "rationale": "The published bound is 40 to 4096 characters, and anything near the floor cannot contain the audience, content and opt-in source a reviewer needs. Enforcing the real bound rather than a guessed heuristic also prevents the opposite failure, where a long, thorough description is silently truncated on submission.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.description",
      "severity": "BLOCKING",
      "detectability": [
        "DETERMINISTIC"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "Twilio",
        "TCR"
      ],
      "codes": [
        {
          "provider": "Twilio",
          "code": "30886",
          "remediable": true,
          "generation": "gen1"
        }
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Expand the description to explain who receives the messages, what they contain, and how recipients opted in. Aim well above the 40-character floor.",
      "example": "Acme Coffee sends weekly promotional offers, new product announcements, and loyalty rewards updates to customers who opted in via the checkout consent checkbox at acmecoffee.com.",
      "notes": "The current contentFlags scanner uses a 40/100 heuristic with no upper bound — this replaces the guess with the published bound.",
      "catalogIds": [
        "CMP-001"
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-EMOJI",
      "slug": "cmp-emoji",
      "title": "Registration fields must not contain emoji",
      "statement": "No campaign registration field may contain an emoji character.",
      "rationale": "Emoji are fine in live message traffic but rejected inside the registration record itself, because the downstream systems that parse and display campaign metadata do not handle them consistently. It surfaces only in the DCA vetting reason list rather than in any provider documentation, which is why generic implementations never check for it — and why a generator that writes a cheerful emoji into a description silently creates a rejection.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.* (all text fields)",
      "severity": "BLOCKING",
      "detectability": [
        "DETERMINISTIC"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "TCR"
      ],
      "codes": [
        {
          "provider": "TCR/DCA",
          "code": "6103",
          "remediable": true
        }
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Remove emoji from the registration fields. They are allowed in live message traffic, but not in the registration record itself.",
      "example": "Acme Coffee sends weekly promotional offers to customers who opted in at checkout.",
      "notes": "Surfaces only in the DCA vetting reason list, not in any provider doc — which is why generic implementations miss it entirely.",
      "catalogIds": [
        "CMP-015"
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-FLOW-LEN",
      "slug": "cmp-flow-len",
      "title": "Message flow must be 40–2049 characters",
      "statement": "The opt-in / message-flow field must be at least 40 and at most 2049 characters.",
      "rationale": "The message flow has to hold the opt-in location, the consent wording, and a reference to the evidence, which does not fit in a sentence. The 2049-character ceiling is tighter than the description's, so a thorough flow can be truncated on submission without warning.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.message_flow",
      "severity": "BLOCKING",
      "detectability": [
        "DETERMINISTIC"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "Twilio",
        "TCR"
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Describe where and how consent is collected, including the exact disclosure wording shown to the consumer.",
      "example": "Customers opt in at https://acmecoffee.com/signup by ticking an unchecked box reading \"By checking this box, you agree to receive recurring automated marketing text messages from Acme Coffee…\". Screenshot: https://cdn.acmecoffee.com/compliance/optin.png",
      "catalogIds": [
        "CMP-002"
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-HTTPS",
      "slug": "cmp-https",
      "title": "Every URL must use HTTPS",
      "statement": "All URLs in the registration must use https://; plain http:// is rejected.",
      "rationale": "A plain http:// link exposes the recipient to interception on exactly the channel carriers are trying to make trustworthy, so it is refused. This is the single highest-consequence cheap defect in the catalog: Twilio's current code for it is NOT eligible for resubmission, so one forgotten \"s\" can permanently kill a campaign rather than earning a fixable rejection.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.sample[] + campaign.message_flow + urls",
      "severity": "BLOCKING",
      "detectability": [
        "DETERMINISTIC"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "Twilio",
        "Infobip",
        "TCR",
        "Bandwidth"
      ],
      "codes": [
        {
          "provider": "Twilio",
          "code": "30964",
          "remediable": false,
          "generation": "gen2"
        },
        {
          "provider": "Infobip",
          "code": "10900",
          "remediable": true
        }
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Replace every http:// link with its https:// equivalent and confirm the certificate is valid.",
      "example": "https://acmecoffee.com/sale",
      "notes": "Twilio 30964 is NOT eligible for resubmission — a plain http:// link can permanently kill a campaign. Highest severity-to-effort ratio in the whole catalog.",
      "catalogIds": [
        "MSG-097"
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-KEYWORD-FMT",
      "slug": "cmp-keyword-fmt",
      "title": "Opt-out and HELP keywords must be alphanumeric and under 255 characters",
      "statement": "Opt-out and help keyword lists must contain only alphanumeric keywords and must not exceed 255 characters.",
      "rationale": "Keywords are matched as single alphanumeric tokens, so a keyword containing a space or punctuation can never be triggered by an inbound message — it silently never works. The product's own form placeholder currently suggests a non-conforming value, which propagates the defect into every registration that accepts the default.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.optout_keywords + campaign.help_keywords",
      "severity": "BLOCKING",
      "detectability": [
        "DETERMINISTIC"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "Twilio",
        "TCR"
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Use plain alphanumeric keywords separated by commas, e.g. \"STOP, UNSUBSCRIBE, CANCEL\".",
      "example": "STOP, QUIT, END, REVOKE, OPTOUT, CANCEL, UNSUBSCRIBE",
      "notes": "The form placeholder currently suggests \"OPT OUT\" style keywords with a space, which violates this rule — the product ships a non-conforming exemplar.",
      "catalogIds": [
        "CMP-011",
        "CMP-012"
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-LINK-DECL",
      "slug": "cmp-link-decl",
      "title": "embeddedLink must match whether samples actually contain links",
      "statement": "The embeddedLink attribute must be true when any sample contains a URL, and a sample must contain a URL when it is declared true.",
      "rationale": "The embedded-link attribute drives carrier filtering, so a mismatch between the declaration and the samples misroutes the campaign's traffic. It fails in both directions: undeclared links look like evasion, and a declared-but-absent link means the campaign was registered for treatment it does not need.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.embedded_link + campaign.sample[]",
      "severity": "BLOCKING",
      "detectability": [
        "DETERMINISTIC"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "TCR",
        "Twilio",
        "AWS",
        "Bandwidth"
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Set the \"messages contain links\" flag to match your sample messages, in both directions.",
      "example": "Samples contain https://acmecoffee.com/sale → set \"messages contain links\" to true.",
      "notes": "Bidirectional. The current contentFlags scanner only checks one direction, and does so with a stateful /g regex.",
      "catalogIds": [
        "MSG-118"
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-PHONE-DECL",
      "slug": "cmp-phone-decl",
      "title": "embeddedPhone must match whether samples contain phone numbers",
      "statement": "The embeddedPhone attribute must be true when any sample contains a phone number, and vice versa.",
      "rationale": "Phone numbers inside message bodies are a filtering signal because they are a common redirection tactic, so the declaration must match reality. Several providers carve out a support number in the HELP reply, which is why this is graded below the link rule.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.embedded_phone + campaign.sample[]",
      "severity": "HIGH",
      "detectability": [
        "DETERMINISTIC"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "TCR",
        "Twilio",
        "AWS"
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Set the \"messages contain phone numbers\" flag to match your sample messages.",
      "example": "Samples contain 415-555-0134 → set \"messages contain phone numbers\" to true.",
      "notes": "A HELP-reply support number is an accepted exception at several providers; that carve-out is not yet modelled here.",
      "catalogIds": [
        "MSG-116"
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-PLACEHOLDER",
      "slug": "cmp-placeholder",
      "title": "Registration fields must not contain placeholder text",
      "statement": "No registration field may contain lorem-ipsum, obvious placeholder text, or unreplaced template tokens.",
      "rationale": "Placeholder text and unreplaced template tokens tell a reviewer the registration was never finished, and they correlate with campaigns that later send something entirely different. An unreplaced \"[Your Business]\" is worse than a blank field because it looks deliberate.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.* (all text fields)",
      "severity": "BLOCKING",
      "detectability": [
        "DETERMINISTIC"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "Twilio",
        "TCR",
        "AWS",
        "Bandwidth"
      ],
      "codes": [
        {
          "provider": "Twilio",
          "code": "30893",
          "remediable": true,
          "generation": "gen1"
        }
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Replace placeholder text with the real content the campaign will use.",
      "example": "Acme Coffee: your order #4471 is ready for pickup. Reply STOP to opt out, HELP for help.",
      "catalogIds": [
        "MSG-022"
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-SAMPLE-COUNT",
      "slug": "cmp-sample-count",
      "title": "At least two sample messages are required",
      "statement": "A campaign must carry at least two, and at most five, sample messages.",
      "rationale": "Two samples is the operative minimum across providers even where a form claims one is enough — AWS's own form says \"at least one\" while its reviewer rejects below two. The upper bound of five is a hard field limit.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.sample[]",
      "severity": "BLOCKING",
      "detectability": [
        "DETERMINISTIC"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "Twilio",
        "TCR",
        "AWS",
        "Bandwidth"
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Provide at least two distinct sample messages that reflect what the campaign will actually send.",
      "example": "Provide 2 to 5 distinct sample messages.",
      "notes": "AWS's own form says \"at least one\" while its reviewer rejects below two. Two is the operative minimum across providers.",
      "catalogIds": [
        "CMP-006",
        "CMP-136",
        "CMP-138"
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-SAMPLE-DUP",
      "slug": "cmp-sample-dup",
      "title": "Sample messages must not be duplicates",
      "statement": "Sample messages must be materially different from one another.",
      "rationale": "Identical samples pad a field count without showing a reviewer anything about the program. This catches the exact-duplicate case cheaply in code; the harder question of whether two differently-worded samples are materially different is judged separately by MSG-SAMPLES-DISTINCT.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.sample[]",
      "severity": "HIGH",
      "detectability": [
        "DETERMINISTIC"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "Twilio",
        "TCR"
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Replace duplicated samples with genuinely different messages the campaign will send.",
      "example": "Each sample should describe a different scenario: a promotion, an order update, an event.",
      "catalogIds": [
        "CMP-149"
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-SAMPLE-LEN",
      "slug": "cmp-sample-len",
      "title": "Each sample message must be 20–1024 characters",
      "statement": "Every sample message must be at least 20 and at most 1024 characters.",
      "rationale": "A sample under 20 characters cannot demonstrate a real message, and one over 1024 is not a text message at all. Both bounds are published and enforced mechanically at submission.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.sample[]",
      "severity": "BLOCKING",
      "detectability": [
        "DETERMINISTIC"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "Twilio",
        "TCR"
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Write realistic sample messages of at least 20 characters each.",
      "example": "Acme Coffee: 20% off all beans this weekend. Reply STOP to opt out, HELP for help.",
      "catalogIds": [
        "CMP-005"
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-URL-DISTINCT",
      "slug": "cmp-url-distinct",
      "title": "Privacy policy and terms URLs must be different pages",
      "statement": "The privacy policy URL and the terms of service URL must not be the same page.",
      "rationale": "The privacy policy and the terms answer different questions — how data is handled versus what the program is — and reviewers check them separately. Brands routinely paste the same combined-legal-page URL into both fields, and a presence-only check passes it while a reviewer opening both links does not.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.privacy_url + campaign.terms_url",
      "severity": "HIGH",
      "detectability": [
        "DETERMINISTIC"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "TCR",
        "Twilio"
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Link the privacy policy and the terms of service separately. If you only have one combined page, publish a distinct SMS terms page.",
      "example": "Privacy: https://acmecoffee.com/privacy · Terms: https://acmecoffee.com/terms",
      "notes": "Catalog POL-017. Brands paste the same URL twice constantly and \"both fields present\" passes today.",
      "catalogIds": [
        "POL-017"
      ],
      "phase": "approval",
      "automated": true
    },
    {
      "id": "CMP-USECASE-ENUM",
      "slug": "cmp-usecase-enum",
      "title": "Use case must be a recognised TCR value",
      "statement": "The declared use case must be one of the recognised TCR use-case values.",
      "rationale": "The use case drives the consent standard, the throughput granted, and the filtering applied, so an unrecognised value cannot be processed at all. Free-text or dropdown-label values coerced silently to a catch-all are worse than an error, because the campaign is then judged against rules nobody chose.",
      "layer": "CAMPAIGN",
      "layerSlug": "campaign",
      "object": "campaign.usecase",
      "severity": "BLOCKING",
      "detectability": [
        "DETERMINISTIC"
      ],
      "failureClass": "RETRY_FIELD",
      "authorities": [
        "TCR",
        "Twilio",
        "Bandwidth",
        "AWS"
      ],
      "applicabilityText": "Applies to every 10DLC registration.",
      "universal": true,
      "remediation": "Select a use case from the standard TCR list rather than typing a description. If none fits exactly, MIXED is the catch-all — but it carries promotional-grade consent obligations.",
      "example": "Use case: MARKETING",
      "pitfalls": [
        "Silent coercion of an unrecognised value to MIXED hides the problem and changes which rules apply."
      ],
      "notes": "Carries a descriptive id rather than a catalog one because no catalog row states this requirement; it is the precondition the use-case matrix rules depend on. It previously carried the id CMP-140, which belongs to an unrelated row (\"read mnoMetadata.minMsgSamples per MNO\") and made that row read as covered when nothing implements it.",
      "phase": "approval",
      "automated": true
    }
  ]
}
