agents/openai.yaml
interface: display_name: "Sent 10DLC Registration" short_description: "Review US A2P registration evidence" default_prompt: "Use $sms-10dlc-registration to review my 10DLC evidence packet for gaps."
sentdm/sent-plugin · GitHub
Prepares and validates Sent US A2P 10DLC brand and campaign registration through Sender Profiles, including inheritance, all campaign use cases, opt-in evidence, sample-message policy, autoresponses, sandbox validation, TCR status, and rejection remediation.
프로젝트 폴더에서 아래 명령어를 실행하고, 설치할 에이전트를 선택하세요.
npx skills add sentdm/sent-plugin --skill sms-10dlc-registration설치 명령을 직접 실행해야 적용됩니다. 지원 에이전트와 필요한 권한·라이선스는 제작자의 안내를 확인하세요.
agents/openai.yamlinterface: display_name: "Sent 10DLC Registration" short_description: "Review US A2P registration evidence" default_prompt: "Use $sms-10dlc-registration to review my 10DLC evidence packet for gaps."
references/10dlc-evidence-checklist.md# 10DLC evidence checklist The evidence packet is an internal readiness artifact with `schema_version: "sent-10dlc-evidence/v1"`. Its snake_case fields are not the Sent API contract. ## Business identity - [ ] Legal business name and EIN match registration records. - [ ] Public website represents the same recognizable brand. - [ ] Physical address, business phone, and compliance contact are current. - [ ] Privacy policy and terms links are public HTTPS pages. ## Consent - [ ] Opt-in method is named: web form, keyword, paper, or spoken consent. - [ ] Proof URL or equivalent evidence is accessible to a reviewer. - [ ] Consent text names the brand and message type. - [ ] The checkbox is not preselected. - [ ] Frequency, message/data-rate notice, HELP, and STOP disclosures are present where required. - [ ] Marketing spoken consent includes the additional confirmation required by policy. ## Message flow Describe the sequence from consumer action through confirmation and recurring messages. A link alone is not a message flow. ## Autoresponses - [ ] `optinMessage` names the brand and explains HELP/STOP, frequency, and rates where required. - [ ] `optoutMessage` confirms unsubscribe and no further messages. - [ ] `helpMessage` names the brand and provides a support method. - [ ] `optinKeywords`, `optoutKeywords`, and `helpKeywords` are documented. - [ ] STOP is in opt-out keywords and HELP is in help keywords. ## Campaign translation Map evidence into these Sent camelCase fields only at API serialization time: ```text message_flow -> messageFlow privacy_policy_url -> privacyPolicyLink terms_and_conditions_url -> termsAndConditionsLink autoresponses.optinMessage -> optinMessage use_cases[].messaging_use_case_us -> useCases[].messagingUseCaseUs use_cases[].sample_messages -> useCases[].sampleMessages ``` Run the evidence validator before translation and the campaign validator after translation.
references/10dlc-rejection-remediation.md# 10DLC rejection remediation ## Diagnose the layer | Layer | Evidence | | --- | --- | | Local evidence packet | Version, missing consent proof, invalid URLs, incomplete autoresponses | | Sent request validation | Wrong camelCase, unsupported field, invalid use case, sample count/length, volume type | | TCR submission | `submittedToTCR`, registry error/reason, brand or campaign status | | Carrier operations | Campaign active but filtering, DCA election, content mismatch | Do not create a new campaign until the failing layer is known. ## Common fixes - Wrong endpoint model: operate campaigns under the profile and create a dedicated brand within profile creation. - Inheritance conflict: disable `inherit_tcr_campaign` before managing a dedicated campaign; do not supply `brand` while brand inheritance is true. - Wrong field casing: serialize `useCases`, `messagingUseCaseUs`, and `sampleMessages` exactly. - Too few samples: provide 1–5 structurally; provide at least two for marketing/mixed compliance. - Weak opt-in: add observable proof and a complete message flow. - Autoresponse failure: add brand, STOP/HELP, unsubscribe confirmation, and support details as applicable. - Traffic mismatch: select a use case that matches actual content rather than rewriting only the example. ## Status interpretation `SENT_CREATED` means a Sent campaign record exists. `submittedToTCR: true` records registry submission. `ACTIVE` means operational activation, while `EXPIRED` is not send-ready. Preserve unknown status strings and any `tcrSyncError` rather than replacing them with a guessed category. ## Resubmission 1. Save the original response and reason. 2. Fix the versioned evidence packet if necessary. 3. Rebuild and validate the API request. 4. Use `sandbox: true`. 5. Show the exact diff. 6. Obtain confirmation before the real mutation.
references/tcr-use-cases.md# TCR use cases ## Current Sent API values | Value | Typical traffic | | --- | --- | | `MARKETING` | Promotions, offers, product announcements | | `ACCOUNT_NOTIFICATION` | Account changes, balances, non-security notices | | `CUSTOMER_CARE` | Support conversations and case updates | | `FRAUD_ALERT` | Suspected fraud notifications | | `TWO_FA` | One-time passcodes and two-factor authentication | | `DELIVERY_NOTIFICATION` | Shipment and delivery updates | | `SECURITY_ALERT` | Security events distinct from general account notices | | `M2M` | Machine-to-machine operational traffic | | `MIXED` | Multiple standard use cases in one campaign | | `HIGHER_EDUCATION` | College or university communications | | `POLLING_VOTING` | Polling and voting interactions | | `PUBLIC_SERVICE_ANNOUNCEMENT` | Public-interest or government notices | | `LOW_VOLUME` | Low-volume mixed traffic | Select what the business will actually send. Do not use `MIXED` or `LOW_VOLUME` to hide a dominant high-risk use case. ## Sample rules The API structure permits 1–5 samples per use case, up to 1,024 characters each. Compliance policy requires at least two samples for `MARKETING`, `MIXED`, and low-volume mixed traffic. Samples should: - name the recognizable consumer brand; - look like real production messages; - match the selected use case and described message flow; - include opt-out language when applicable; - use synthetic names, codes, numbers, and URLs. ## Volume `volume` is a numeric string. Values below `2000` are in the documented low-volume tier. Exactly `2000` crosses the tier boundary, so validate `1999` and `2000` separately.
scripts/fixtures/bad.json{
"schema_version": "unversioned-packet",
"legal_business_name": "",
"ein": "123",
"business_phone": "2025550100",
"contact_email": "invalid",
"website": "not-a-public-url",
"privacy_policy_url": "missing-scheme.example",
"terms_and_conditions_url": "",
"opt_in_evidence": {},
"message_flow": "",
"autoresponses": {
"optinMessage": "",
"optoutMessage": "",
"helpMessage": "",
"optinKeywords": "YES",
"optoutKeywords": "UNSUBSCRIBE",
"helpKeywords": "INFO"
},
"use_cases": []
}
scripts/fixtures/campaign_bad.json{
"campaign": {
"name": "Bad campaign",
"description": "Uses the wrong field names and too few marketing samples.",
"type": "App",
"use_cases": [],
"useCases": [
{
"messagingUseCaseUs": "MARKETING",
"sampleMessages": ["One sample only"]
}
],
"volume": 2000
},
"sandbox": true
}
scripts/fixtures/campaign_good.json{
"campaign": {
"name": "Acme account notifications",
"description": "Synthetic account and delivery notifications for opted-in customers.",
"type": "App",
"useCases": [
{
"messagingUseCaseUs": "ACCOUNT_NOTIFICATION",
"sampleMessages": [
"Acme Example: Your account preference was updated. Reply STOP to opt out."
]
}
],
"volume": "2000",
"messageFlow": "Customers opt in through the account settings form before notifications begin.",
"privacyPolicyLink": "https://example.com/privacy",
"termsAndConditionsLink": "https://example.com/terms",
"optinMessage": "Acme Example: You are subscribed. Reply STOP to opt out.",
"optoutMessage": "Acme Example: You are unsubscribed and will receive no more messages.",
"helpMessage": "Acme Example: Visit https://example.com/support for help.",
"optinKeywords": "START,YES",
"optoutKeywords": "STOP,UNSUBSCRIBE",
"helpKeywords": "HELP,INFO"
},
"sandbox": true
}
scripts/fixtures/good.json{
"schema_version": "sent-10dlc-evidence/v1",
"legal_business_name": "Acme Example LLC",
"ein": "12-3456789",
"business_address": "123 Example Street, New York, NY 10001, US",
"business_phone": "+12025550100",
"contact_email": "compliance@example.com",
"website": "https://example.com",
"privacy_policy_url": "https://example.com/privacy",
"terms_and_conditions_url": "https://example.com/terms",
"opt_in_evidence": {
"method": "web_form",
"description": "A synthetic checkout form uses an unchecked SMS consent box.",
"proof_url": "https://example.com/sms-consent"
},
"message_flow": "Customers submit the web form and receive the opt-in confirmation before notifications begin.",
"autoresponses": {
"optinMessage": "Acme Example: You are subscribed. Msg frequency varies. Msg and data rates may apply. Reply HELP for help or STOP to opt out.",
"optoutMessage": "Acme Example: You are unsubscribed and will receive no more messages.",
"helpMessage": "Acme Example: Visit https://example.com/support for help.",
"optinKeywords": "START,YES",
"optoutKeywords": "STOP,UNSUBSCRIBE",
"helpKeywords": "HELP,INFO"
},
"use_cases": [
{
"messaging_use_case_us": "MARKETING",
"sample_messages": [
"Acme Example: Save 10% on your next order. Reply STOP to opt out.",
"Acme Example: New items are available. Reply STOP to opt out."
]
}
]
}
scripts/validate_10dlc_packet.py#!/usr/bin/env python3
"""Validate the internal 10DLC evidence packet schema.
This packet is readiness evidence, not the Sent campaign API request. Its
snake_case fields are namespaced by an explicit schema version so they cannot
be mistaken for Sent's camelCase contract.
Exit codes:
0 - valid evidence packet
1 - invalid packet or unreadable/malformed input
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from pathlib import Path
from typing import Any
SCHEMA_VERSION = "sent-10dlc-evidence/v1"
USE_CASES = {
"MARKETING",
"ACCOUNT_NOTIFICATION",
"CUSTOMER_CARE",
"FRAUD_ALERT",
"TWO_FA",
"DELIVERY_NOTIFICATION",
"SECURITY_ALERT",
"M2M",
"MIXED",
"HIGHER_EDUCATION",
"POLLING_VOTING",
"PUBLIC_SERVICE_ANNOUNCEMENT",
"LOW_VOLUME",
}
URL_RE = re.compile(r"^https://[^\s]+$", re.IGNORECASE)
EIN_RE = re.compile(r"^\d{2}-?\d{7}$")
PHONE_RE = re.compile(r"^\+\d{1,15}$")
EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
REQUIRED = {
"schema_version",
"legal_business_name",
"ein",
"business_address",
"business_phone",
"contact_email",
"website",
"privacy_policy_url",
"terms_and_conditions_url",
"opt_in_evidence",
"message_flow",
"autoresponses",
"use_cases",
}
def _text(value: Any) -> bool:
return isinstance(value, str) and bool(value.strip())
def validate(packet: Any, path: str = "<packet>") -> list[str]:
issues: list[str] = []
def issue(field: str, reason: str) -> None:
issues.append(f"{path}: {field}: {reason}")
if not isinstance(packet, dict):
issue("<root>", "must be a JSON object")
return issues
for key in sorted(REQUIRED - set(packet)):
issue(key, "missing required field")
if packet.get("schema_version") != SCHEMA_VERSION:
issue("schema_version", f"must equal {SCHEMA_VERSION!r}")
for key in ("legal_business_name", "business_address", "message_flow"):
if key in packet and not _text(packet[key]):
issue(key, "must be a non-empty string")
if _text(packet.get("ein")) and not EIN_RE.fullmatch(packet["ein"]):
issue("ein", "must contain nine digits, optionally formatted NN-NNNNNNN")
if _text(packet.get("business_phone")) and not PHONE_RE.fullmatch(packet["business_phone"]):
issue("business_phone", "must be E.164")
if _text(packet.get("contact_email")) and not EMAIL_RE.fullmatch(packet["contact_email"]):
issue("contact_email", "must be a valid email")
for key in ("website", "privacy_policy_url", "terms_and_conditions_url"):
if key in packet and (not _text(packet[key]) or not URL_RE.fullmatch(packet[key])):
issue(key, "must be a public HTTPS URL")
opt_in = packet.get("opt_in_evidence")
if not isinstance(opt_in, dict):
issue("opt_in_evidence", "must be an object")
else:
for key in ("method", "description", "proof_url"):
if not _text(opt_in.get(key)):
issue(f"opt_in_evidence.{key}", "must be a non-empty string")
proof = opt_in.get("proof_url")
if _text(proof) and not URL_RE.fullmatch(proof):
issue("opt_in_evidence.proof_url", "must be a public HTTPS URL")
autoresponses = packet.get("autoresponses")
if not isinstance(autoresponses, dict):
issue("autoresponses", "must be an object")
else:
for key in ("optinMessage", "optoutMessage", "helpMessage", "optinKeywords", "optoutKeywords", "helpKeywords"):
if not _text(autoresponses.get(key)):
issue(f"autoresponses.{key}", "must be a non-empty string")
if "STOP" not in str(autoresponses.get("optoutKeywords", "")).upper().split(","):
issue("autoresponses.optoutKeywords", "must include STOP")
if "HELP" not in str(autoresponses.get("helpKeywords", "")).upper().split(","):
issue("autoresponses.helpKeywords", "must include HELP")
use_cases = packet.get("use_cases")
if not isinstance(use_cases, list) or not use_cases:
issue("use_cases", "must be a non-empty array")
else:
for index, use_case in enumerate(use_cases):
field = f"use_cases[{index}]"
if not isinstance(use_case, dict):
issue(field, "must be an object")
continue
if use_case.get("messaging_use_case_us") not in USE_CASES:
issue(f"{field}.messaging_use_case_us", f"must be one of {sorted(USE_CASES)}")
samples = use_case.get("sample_messages")
if not isinstance(samples, list) or not 1 <= len(samples) <= 5:
issue(f"{field}.sample_messages", "must contain 1–5 samples")
elif any(not _text(sample) or len(sample) > 1024 for sample in samples):
issue(f"{field}.sample_messages", "samples must be non-empty strings of at most 1,024 characters")
return issues
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("packet", type=Path)
args = parser.parse_args(argv)
try:
packet = json.loads(args.packet.read_text(encoding="utf-8"))
except OSError as exc:
print(f"{args.packet}: <file>: {exc}", file=sys.stderr)
return 1
except json.JSONDecodeError as exc:
print(f"{args.packet}: <file>: invalid JSON ({exc})", file=sys.stderr)
return 1
issues = validate(packet, str(args.packet))
if issues:
print("\n".join(issues), file=sys.stderr)
return 1
print("OK")
return 0
if __name__ == "__main__":
raise SystemExit(main())
scripts/validate_campaign_payload.py#!/usr/bin/env python3
"""Validate the exact Sent campaign request used by profile campaign endpoints.
Exit codes:
0 - valid campaign payload
1 - invalid payload or unreadable/malformed input
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from pathlib import Path
from typing import Any
USE_CASES = {
"MARKETING",
"ACCOUNT_NOTIFICATION",
"CUSTOMER_CARE",
"FRAUD_ALERT",
"TWO_FA",
"DELIVERY_NOTIFICATION",
"SECURITY_ALERT",
"M2M",
"MIXED",
"HIGHER_EDUCATION",
"POLLING_VOTING",
"PUBLIC_SERVICE_ANNOUNCEMENT",
"LOW_VOLUME",
}
POLICY_TWO_SAMPLE_CASES = {"MARKETING", "MIXED", "LOW_VOLUME"}
CAMPAIGN_REQUIRED = {"name", "description", "type", "useCases"}
CAMPAIGN_OPTIONAL = {
"volume",
"messageFlow",
"privacyPolicyLink",
"termsAndConditionsLink",
"optinMessage",
"optoutMessage",
"helpMessage",
"optinKeywords",
"optoutKeywords",
"helpKeywords",
}
URL_RE = re.compile(r"^https://[^\s]+$", re.IGNORECASE)
VOLUME_RE = re.compile(r"^\d+$")
def _text(value: Any) -> bool:
return isinstance(value, str) and bool(value.strip())
def validate(payload: Any, path: str = "<payload>") -> list[str]:
issues: list[str] = []
def issue(field: str, reason: str) -> None:
issues.append(f"{path}: {field}: {reason}")
if not isinstance(payload, dict):
issue("<root>", "must be a JSON object")
return issues
for key in sorted(set(payload) - {"campaign", "sandbox"}):
issue(key, "unsupported top-level field")
if "sandbox" in payload and not isinstance(payload["sandbox"], bool):
issue("sandbox", "must be boolean")
campaign = payload.get("campaign")
if not isinstance(campaign, dict):
issue("campaign", "required and must be an object")
return issues
allowed = CAMPAIGN_REQUIRED | CAMPAIGN_OPTIONAL
for key in sorted(set(campaign) - allowed):
issue(f"campaign.{key}", "unsupported field; use the exact camelCase Sent contract")
for key in sorted(CAMPAIGN_REQUIRED - set(campaign)):
issue(f"campaign.{key}", "missing required field")
for key in ("name", "description", "type"):
if key in campaign and not _text(campaign[key]):
issue(f"campaign.{key}", "must be a non-empty string")
volume = campaign.get("volume")
if volume is not None and (not isinstance(volume, str) or not VOLUME_RE.fullmatch(volume)):
issue("campaign.volume", "must be a numeric string such as '1999' or '2000'")
for key in ("privacyPolicyLink", "termsAndConditionsLink"):
value = campaign.get(key)
if value is not None and (not _text(value) or not URL_RE.fullmatch(value)):
issue(f"campaign.{key}", "must be a public HTTPS URL or null")
for key in ("optinKeywords", "optoutKeywords", "helpKeywords"):
value = campaign.get(key)
if value is not None and (not _text(value) or len(value) > 255):
issue(f"campaign.{key}", "must be a non-empty string of at most 255 characters or null")
use_cases = campaign.get("useCases")
if not isinstance(use_cases, list) or not use_cases:
issue("campaign.useCases", "must be a non-empty array")
return issues
for index, use_case in enumerate(use_cases):
field = f"campaign.useCases[{index}]"
if not isinstance(use_case, dict):
issue(field, "must be an object")
continue
for key in sorted(set(use_case) - {"messagingUseCaseUs", "sampleMessages"}):
issue(f"{field}.{key}", "unsupported field")
selected = use_case.get("messagingUseCaseUs")
if selected not in USE_CASES:
issue(f"{field}.messagingUseCaseUs", f"must be one of {sorted(USE_CASES)}")
samples = use_case.get("sampleMessages")
if not isinstance(samples, list) or not 1 <= len(samples) <= 5:
issue(f"{field}.sampleMessages", "must contain 1–5 samples")
continue
for sample_index, sample in enumerate(samples):
if not _text(sample):
issue(f"{field}.sampleMessages[{sample_index}]", "must be a non-empty string")
elif len(sample) > 1024:
issue(f"{field}.sampleMessages[{sample_index}]", "must be at most 1,024 characters")
if selected in POLICY_TWO_SAMPLE_CASES and len(samples) < 2:
issue(
f"{field}.sampleMessages",
f"{selected} requires at least two samples under the compliance policy layer",
)
return issues
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("payload", type=Path)
args = parser.parse_args(argv)
try:
payload = json.loads(args.payload.read_text(encoding="utf-8"))
except OSError as exc:
print(f"{args.payload}: <file>: {exc}", file=sys.stderr)
return 1
except json.JSONDecodeError as exc:
print(f"{args.payload}: <file>: invalid JSON ({exc})", file=sys.stderr)
return 1
issues = validate(payload, str(args.payload))
if issues:
print("\n".join(issues), file=sys.stderr)
return 1
print("OK")
return 0
if __name__ == "__main__":
raise SystemExit(main())
SKILL.md---
name: sms-10dlc-registration
description: Prepares and validates Sent US A2P 10DLC brand and campaign registration through Sender Profiles, including inheritance, all campaign use cases, opt-in evidence, sample-message policy, autoresponses, sandbox validation, TCR status, and rejection remediation.
---
# SMS 10DLC Registration
Use this skill for US A2P SMS over 10-digit long codes. Separate the compliance evidence packet from the exact Sent API request; they have different schemas and validators.
## Current Sent resource model
There is no standalone brand CRUD path in the current v3 API.
- Create a dedicated brand inside `POST /v3/profiles` using `brand` and `inherit_tcr_brand: false`.
- List/create campaigns with `GET|POST /v3/profiles/{profileId}/campaigns`.
- Update/delete with `PUT|DELETE /v3/profiles/{profileId}/campaigns/{campaignId}`.
Reject guidance that reintroduces a free-standing brand path.
## Choose inheritance deliberately
| Brand | Campaign | Settings |
| --- | --- | --- |
| Inherit both | Organization brand and campaign | `inherit_tcr_brand: true`, `inherit_tcr_campaign: true` |
| Inherit brand, own campaign | Shared legal brand with tenant-specific traffic | brand true, campaign false |
| Own both | Dedicated tenant/business | both false and supply `brand` during profile creation |
Inherited campaigns are read-only. A profile cannot supply `brand` while brand inheritance is true.
## Two validation layers
### Evidence readiness packet
The private packet uses the explicit internal version `sent-10dlc-evidence/v1` and snake_case evidence fields. It is not an API payload.
```bash
python scripts/validate_10dlc_packet.py evidence.json
```
Collect legal identity, public website/policy links, consent proof, message flow, opt-in/opt-out/help responses and keywords, use cases, and realistic samples. See [references/10dlc-evidence-checklist.md](references/10dlc-evidence-checklist.md).
### Sent campaign request
The API request uses exact camelCase and a `campaign` wrapper:
<!-- sent-campaign-request -->
```json
{
"campaign": {
"name": "Acme account notifications",
"description": "Account and delivery notifications for opted-in customers.",
"type": "App",
"useCases": [
{
"messagingUseCaseUs": "ACCOUNT_NOTIFICATION",
"sampleMessages": [
"Acme Example: Your account preference was updated. Reply STOP to opt out."
]
}
],
"volume": "2000",
"messageFlow": "Customers opt in in account settings before notifications begin.",
"privacyPolicyLink": "https://example.com/privacy",
"termsAndConditionsLink": "https://example.com/terms",
"optinMessage": "Acme Example: You are subscribed. Reply STOP to opt out.",
"optoutMessage": "Acme Example: You are unsubscribed and will receive no more messages.",
"helpMessage": "Acme Example: Visit https://example.com/support for help.",
"optinKeywords": "START,YES",
"optoutKeywords": "STOP,UNSUBSCRIBE",
"helpKeywords": "HELP,INFO"
},
"sandbox": true
}
```
Validate it with:
```bash
python scripts/validate_campaign_payload.py campaign.json
```
## API use cases
Support all 13 current values:
`MARKETING`, `ACCOUNT_NOTIFICATION`, `CUSTOMER_CARE`, `FRAUD_ALERT`, `TWO_FA`, `DELIVERY_NOTIFICATION`, `SECURITY_ALERT`, `M2M`, `MIXED`, `HIGHER_EDUCATION`, `POLLING_VOTING`, `PUBLIC_SERVICE_ANNOUNCEMENT`, and `LOW_VOLUME`.
Each use case structurally accepts 1–5 samples, each no longer than 1,024 characters. The compliance layer requires at least two samples for marketing and mixed traffic, including low-volume mixed. Keep that policy distinction visible instead of pretending OpenAPI requires two for all traffic.
## Volume and status
`volume` is optional and, when supplied, is a numeric string. Values below `"2000"` use the documented low-volume tier; `"2000"` is the boundary to the next tier.
Campaign responses currently expose statuses `SENT_CREATED`, `ACTIVE`, and `EXPIRED`, plus `submittedToTCR`. Preserve unknown future status strings. Do not confuse a successful Sent record creation with TCR submission or carrier activation.
## Safe workflow
1. Confirm this is US A2P 10DLC traffic and the actual sending business is identified.
2. Select brand/campaign inheritance.
3. Validate the versioned evidence packet.
4. Create or confirm the profile brand.
5. Translate evidence into the exact camelCase campaign request.
6. Validate locally and use `sandbox: true`.
7. Show the payload and obtain confirmation before a real create/update/delete.
8. Store profile ID, campaign ID, `submittedToTCR`, raw status, and review evidence.
9. Complete the profile with required `webHookUrl` only after prerequisites are ready.
Never use real consumer data in fixtures or samples. Use [references/tcr-use-cases.md](references/tcr-use-cases.md) for classification and [references/10dlc-rejection-remediation.md](references/10dlc-rejection-remediation.md) for failures.