references/customer-go-to-production-guide.md
# AWS End User Messaging: Customer go-to-production guide
> The onboarding quickstarts run in constrained modes — an RCS test agent reaches only verified
> testers, the SMS sandbox reaches only verified destination numbers under a low spend limit, and
> Notify Basic has per-day and per-country limits. This guide is the production procedures for
> lifting those constraints: exiting the SMS sandbox, launching RCS in a country with carrier
> monitoring and SMS fallback, upgrading Notify to Advanced, and the production hygiene every
> identity needs. It does NOT repeat the registration lifecycle itself (create → derive fields →
> fill → submit → poll → recover from denial) — that engine lives in
> [references/registration-core-engine.md](registration-core-engine.md), and which
> registration a country needs is a live-discovery question answered by
> [references/registration-requirements-by-country.md](registration-requirements-by-country.md).
You run every command yourself. The user provides inputs and confirms results. Production
reviews run from about a day to several months — submit correctly, set expectations, and leave
the user a monitoring loop. Stop and consult the Failure modes table on any error.
## Session state
| Value | Format | Source |
|---|---|---|
| `REGION` | e.g. `us-east-1` | same as the quickstart |
| `VDN_ID` | `vdn-...` | CreateVerifiedDestinationNumber response |
| `PHONE_NUMBER_ID` | `phone-...` | RequestPhoneNumber response |
| `POOL_ID` | `pool-...` | CreatePool response |
| `AGENT_ID` | `rcs-...` | from the RCS quickstart, if continuing |
## Prerequisites
- AWS account with the AWS End User Messaging SMS and Voice service enabled.
- IAM role with least-privilege `sms-voice:` permissions for the actions used below (for example
`DescribeAccountAttributes`, `DescribeSpendLimits`, `SetTextMessageSpendLimitOverride`,
`CreateVerifiedDestinationNumber`, `RequestPhoneNumber`, `CreatePool`,
`AssociateOriginationIdentity`, `PutKeyword`, `DescribeRcsAgentCountryLaunchStatus`,
`DescribeNotifyConfigurations`). Scope the policy to the actions the chosen path uses; do not
grant `sms-voice:*` or `*FullAccess`. Prefer assuming an IAM role with ephemeral credentials
over long-lived access keys.
- Validate all input parameters (region, phone numbers in E.164, ISO country code, spend
amounts) before calling APIs.
- These operations are available through the AWS MCP Server, which provides sandboxed execution,
audit logging, and observability. When the MCP server is not available, use the AWS CLI
commands shown below.
- Confirm the AWS CLI is available (`aws --version`). On CLI v1, URL-valued fields need the
`--cli-input-json` form and `aws logs tail` is unavailable.
## Choose the path
Ask the user what they need to send, to whom, and where:
| The user wants | Path |
|---|---|
| SMS to real customers | A. Exit the SMS sandbox, then get a registered origination identity |
| A registered number, short code, or sender ID | The registration engine — [references/registration-core-engine.md](registration-core-engine.md) |
| RCS to real customers | B. Country launch on the agent, with monitoring and fallback |
| Notify at higher volume or more countries | C. Advanced tier upgrade |
Paths compose: a production RCS agent should sit in a pool with an SMS number for fallback — set
up the SMS identity before or alongside the RCS launch. Never quote a review ETA from memory —
get the current one from the live sources in
[references/registration-requirements-by-country.md](registration-requirements-by-country.md).
## Path A — Exit the SMS sandbox
New accounts start in the SMS sandbox: sends reach only verified destination numbers, and spend
is capped at a low per-month default. This is account-plus-region state, independent of any
registration. Check the current values live:
```bash
aws pinpoint-sms-voice-v2 describe-account-attributes --region <REGION>
aws pinpoint-sms-voice-v2 describe-spend-limits --region <REGION>
```
Exiting is a Support case (quota: "SMS Production Access"), per region — a console step: Support
Center → Create case → Service limit increase → Pinpoint SMS. The case asks for use case
description, website, expected volume, and opt-in process. Answer specifically; vague opt-in
descriptions are the top rejection cause.
While still sandboxed, add test recipients with the three-step verification flow (the code send
requires an ACTIVE origination identity in the account, or use simulator numbers — simulator-to-simulator
only). Provide the destination in E.164 format (e.g., `+12065550123`):
```bash
aws pinpoint-sms-voice-v2 create-verified-destination-number \
--destination-phone-number <PHONE_E164> --region <REGION>
aws pinpoint-sms-voice-v2 send-destination-number-verification-code \
--verified-destination-number-id <VDN_ID> --verification-channel TEXT \
--origination-identity <ACTIVE_ORIGINATOR> --region <REGION>
aws pinpoint-sms-voice-v2 verify-destination-number \
--verified-destination-number-id <VDN_ID> --verification-code <CODE> --region <REGION>
```
Raise the spend limit as part of, or after, the same case — the account limit is a Support
change; the enforced limit is then self-serve. This is a REQUIRED step, not optional: even after
the sandbox is lifted, sends still fail at the low sandbox spend cap until you raise it. Set it
above expected monthly volume:
```bash
aws pinpoint-sms-voice-v2 set-text-message-spend-limit-override --monthly-limit <AMOUNT> --region <REGION>
```
Sandbox exit removes the destination restriction but does NOT provide an origination identity
for countries that require one — that is the registration engine, and whether the destination
country requires one is a live-discovery question:
[references/registration-requirements-by-country.md](registration-requirements-by-country.md).
## Path B — RCS production launch (carrier monitoring and fallback)
The RCS launch registration itself runs through the core engine
([references/registration-core-engine.md](registration-core-engine.md), RCS launch
section — `ASSOCIATE_BEFORE_SUBMIT`). This section covers the production operations AROUND that
registration: monitoring carrier status and setting up SMS fallback. Carrier review runs for
months; set that expectation up front.
Once the launch registration is submitted, monitor per-carrier progress:
```bash
aws pinpoint-sms-voice-v2 describe-rcs-agent-country-launch-status \
--rcs-agent-id <AGENT_ID> --region <REGION>
```
Carrier statuses are `PENDING`, `ACTIVE`, or `REJECTED`; the country aggregate is `PENDING`,
`PARTIAL`, `ACTIVE`, or `REJECTED`. You can send in a country as soon as ONE carrier is
`ACTIVE` — recipients on non-approved carriers get SMS fallback if a pool is set up.
Set up a fallback pool (strongly recommended). The pool must contain BOTH the RCS agent AND at
least one SMS-capable number — an agent-only pool cannot fall back:
```bash
aws pinpoint-sms-voice-v2 create-pool \
--origination-identity <AGENT_ID> --iso-country-code <ISO2> \
--message-type TRANSACTIONAL --region <REGION>
aws pinpoint-sms-voice-v2 associate-origination-identity \
--pool-id <POOL_ID> --origination-identity <PHONE_NUMBER_ID> \
--iso-country-code <ISO2> --region <REGION>
```
Then send via `--origination-identity <POOL_ID>`. Fallback behavior: RCS is tried first; if no
delivery signal within a short interval, SMS goes out and the RCS message is revoked (rare dual
delivery is possible — both billed). Sticky sending pins each destination to its last-working
channel for about a day, with periodic RCS retries. Keywords and the two-way SNS destination
live on the agent (shared across all countries); brand assets live per registration.
**Two fallback mechanisms — pick per use case.** The pool above gives *automatic* fallback: when
you send via `SendTextMessage` through a pool holding the agent + an SMS number, RCS is tried
first and SMS goes out on failure, reusing the same body. For finer control, `SendRcsMessage` also
takes an explicit per-message `--fallback-configuration`:
```bash
aws pinpoint-sms-voice-v2 send-rcs-message \
--destination-phone-number <PHONE_E164> --origination-identity <AGENT_ID> \
--rcs-message-content '{"Content":{"TextMessage":{"Body":"<rich body>"}}}' \
--time-to-live 60 \
--fallback-configuration '{"Channel":"SMS","MessageBody":"<concise SMS copy>","OriginationIdentity":"<PHONE_OR_SENDER_ID>"}' \
--region <REGION>
```
Per-message fallback differs from the pool path: it fires on *immediate* RCS failure OR when a
`--time-to-live` timer expires (useful for OTPs and time-sensitive sends); it can fall back to
**SMS or MMS** (`Channel`); and you supply **separate, tailored** content — the fallback body has a
shorter character limit than the RCS body, so write concise copy and inline any URLs from
suggestion chips. `OriginationIdentity` must be a phone number or sender ID (not a pool or agent);
omit it and a pooled send auto-selects one. Use the pool path for zero-config coarse fallback; use
per-message when you need MMS, tailored copy, or TTL-based expiry. Confirm the current character
limits and field constraints from the linked doc or the API's validation error output rather than
assuming fixed values. See
[RCS per-message fallback](https://docs.aws.amazon.com/sms-voice/latest/userguide/rcs-fallback-per-message.html).
## Path C — Notify Basic to Advanced
Needed only for higher volume or TPS, or countries outside the Basic set. Check
`CustomerOwnedIdentityRequired` via `list-notify-countries` — some Advanced countries also
require your own identity from the registration engine; the country lists are live data (see
[references/registration-requirements-by-country.md](registration-requirements-by-country.md)).
Registration type names drift — before using them, discover the current `NOTIFY_*` type name
live rather than trusting any name verbatim:
```bash
aws pinpoint-sms-voice-v2 describe-registration-type-definitions --region <REGION> \
--query "RegistrationTypeDefinitions[?contains(RegistrationType,'NOTIFY')].RegistrationType"
```
Use the returned tier-upgrade type as `<NOTIFY_UPGRADE_TYPE>` in the create call:
```bash
aws pinpoint-sms-voice-v2 create-registration \
--registration-type <NOTIFY_UPGRADE_TYPE> --region <REGION>
```
A separate brand-verification type (matching `NOTIFY_*` in the discovery output above) resolves a
configuration stuck in `REQUIRES_VERIFICATION`. Derive fields, populate (brand verification and
proof of opt-in flow), and submit through the core engine. Track the upgrade via
`TierUpgradeStatus` on `describe-notify-configurations`: `BASIC`, `PENDING_UPGRADE`, `ADVANCED`,
or `REJECTED`.
## Production hygiene (all paths)
Apply to every production identity:
1. **Keywords** — set HELP (must include a support contact) and STOP (must confirm cessation) on
every long code and short code. `put-keyword` requires `--origination-identity`, `--keyword`,
and `--keyword-message`; `--keyword-action` is optional (`AUTOMATIC_RESPONSE` or `OPT_OUT`):
`aws pinpoint-sms-voice-v2 put-keyword --origination-identity <ID> --keyword HELP --keyword-message "<help text with support contact>" --keyword-action AUTOMATIC_RESPONSE --region <REGION>`
(repeat for the STOP keyword with a cessation-confirmation message, using `--keyword-action OPT_OUT`).
2. **Event visibility** — create a configuration set with an event destination and pass
`--configuration-set-name` on every send. Without it there are NO delivery events, and they
are not retroactive. Use an SNS topic with KMS encryption (`alias/aws/sns` or a customer-managed
key) and add a resource policy with `aws:SourceAccount` and `aws:SourceArn` condition keys to
prevent confused-deputy access. Restrict `sns:Subscribe` via the topic policy to known,
authorized AWS accounts or endpoint patterns so only trusted recipients receive delivery
metadata, and subscribe HTTPS-only endpoints.
3. **Protect configuration** — block countries you never send to.
4. **Pool per use case** — put identities for one use case in one pool and send via the pool;
mixing use cases on shared identities risks carrier filtering.
5. **Resource policy** — required if another service (for example SNS) will use the number, even
in the same account. Include `aws:SourceAccount` and `aws:SourceArn` condition keys in the
policy to prevent confused-deputy access.
6. **Rate-limit outbound sends** — implement application-side throttling (per-recipient and
per-minute caps) to prevent abuse, SMS pumping, and toll fraud, and to stay within service
quotas.
7. **CloudWatch alarms** — create alarms on key metrics: send-failure-rate exceeding a threshold,
monthly spend approaching the `set-text-message-spend-limit-override` value, and throttling
events. Without alarms, failures and cost overruns are detected only after impact. Route alarm
actions to an SNS topic with KMS encryption and the same `aws:SourceAccount`/`aws:SourceArn`
condition keys and authorized-subscriber controls (restricted `sns:Subscribe`, HTTPS-only
subscriptions) as the event destination in item 2 — alarm notifications about failure rates,
spend, and throttling carry operationally sensitive information.
## Verify
Production-readiness checklist — confirm each with a describe call, not from memory:
- [ ] Account attributes show the sandbox removed (Path A).
- [ ] Registration(s) `COMPLETE`; identity `ACTIVE`, or at least one carrier `ACTIVE` for RCS,
or `ADVANCED` for Notify.
- [ ] HELP and STOP keywords set on every production identity.
- [ ] A configuration set is attached to sends and a test send produces a delivery event.
- [ ] Spend limit raised above expected monthly volume.
- [ ] For RCS: the fallback pool contains the agent AND an SMS number.
- [ ] CloudTrail logging enabled for `sms-voice` API calls; CloudTrail logs and CloudWatch Log
groups encrypted with a KMS CMK.
**Demonstrable outcome.** The artifacts are state transitions — capture them as before/after
describe output the user can see: the sandbox attribute clearing, a carrier row moving to
`ACTIVE`, `TierUpgradeStatus` reaching `ADVANCED`, then one test send that produces a delivery
event. Reviews take days to months, so most sessions end mid-transition; when yours does, say so
and leave the user the exact poll command.
## Next steps
- Registration lifecycle mechanics: [references/registration-core-engine.md](registration-core-engine.md)
- Which registration a country needs, and its requirements — always live: [references/registration-requirements-by-country.md](registration-requirements-by-country.md)
- RCS test agent quickstart: [references/hello-rcs-test-agent.md](hello-rcs-test-agent.md)
- Notify OTP quickstart: [references/hello-notify-otp.md](hello-notify-otp.md)
## Security Considerations
- **Least-privilege IAM.** Scope policies to the specific `sms-voice:` actions each path uses;
never use `*FullAccess` or `sms-voice:*`.
- **Ephemeral credentials.** Assume an IAM role with ephemeral credentials; never embed
long-lived access keys in code, config, or environment variables.
- **Event destinations.** Use SNS topics with KMS encryption, `aws:SourceAccount` and
`aws:SourceArn` condition keys, and HTTPS-only subscriptions. Message bodies and delivery
metadata may contain sensitive information — encrypt CloudTrail logs and CloudWatch Log groups
with a KMS CMK.
- **CloudTrail.** Enable CloudTrail logging for `sms-voice` API calls in the account to maintain
an audit trail of production operations — sandbox exits, spend-limit changes, pool associations,
and keyword configurations.
- **Consent and spend.** Message only recipients who have opted in; honor STOP. Raise spend
limits deliberately and monitor them.
- **Input validation.** Validate all user inputs (E.164 for phones, ISO country code, spend
amounts, URL scheme allowlist) before passing them to APIs.
- **AWS security references.** These recommendations follow AWS security best practices — see
[IAM Best Practices](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html) for
least-privilege and ephemeral credentials, and
[AWS KMS Best Practices](https://docs.aws.amazon.com/kms/latest/developerguide/best-practices.html)
for encryption of CloudTrail logs, CloudWatch Log groups, and SNS topics.
## Failure modes
| Error / symptom | Cause | Fix |
|---|---|---|
| Sends fail right after sandbox exit | Spend limit still at the sandbox default | Raise it with `set-text-message-spend-limit-override` (Path A) |
| RCS country send fails, aggregate `PARTIAL` | Recipient's carrier not `ACTIVE`, no fallback pool | Build the Path B fallback pool (agent + SMS number) |
| Throughput stuck at the default MPS after approval | MPS increases are not automatic | Separate Support request for MPS |
| Sends show "NOTICE"/"Unverified" sender | Registration not yet `COMPLETE` | Wait for `COMPLETE`; see the core engine |
| Notify upgrade stuck | Awaiting review, or `REQUIRES_VERIFICATION` | Poll `TierUpgradeStatus`; if verification is required, discover the brand-verification type via `describe-registration-type-definitions` (match `NOTIFY_*` containing `BRAND_VERIFICATION`) and submit through the core engine |
| Sender ID sends fail despite approval | Case-sensitivity mismatch — carriers match exact casing | Use the exact casing from the registration record |
## Cleanup (destructive — confirm with the user first)
Production identities bill monthly while owned. To stop: `release-phone-number`,
`release-sender-id`, or `delete-registration` — all irreversible. Confirm with the user, and
note released numbers may not be recoverable.
references/hello-notify-otp.md
# Hello AWS End User Messaging: Send an OTP with AWS End User Messaging Notify in minutes
> By the end of this task, the user has received a real one-time-passcode text on their phone,
> sent through AWS End User Messaging Notify. No phone number to buy, no carrier registration,
> no wait times — AWS manages the sending identities, routing, and pre-approved message
> templates. Working time is about 5 minutes end to end. This is the fastest possible path to a
> production-grade verification message; the Basic tier works in many countries out of the box (use `list-notify-countries` to see the current set).
You run every command yourself. The user provides inputs and confirms what arrives on their
phone. Stop and consult the Failure modes table on any error.
## Session state
| Value | Format | Source |
|---|---|---|
| `REGION` | e.g. `us-east-1` | ask the user (default `us-east-1`) |
| `PROFILE` | AWS CLI profile name | credential bootstrap (may be empty) |
| `DISPLAY_NAME` | ≤15 chars; letters, digits, `_`, `-`, space | ask the user (their brand/app name) |
| `NC_ID` | `notify-` + 32 hex | CreateNotifyConfiguration response |
| `TEMPLATE_ID` | e.g. `notify-code-verification-english-001` | DescribeNotifyTemplates response |
| `PHONE` | E.164 | ask the user (their real phone) |
## Prerequisites
1. **AWS CLI** — check the version: `aws --version`. v2 recommended; v1 verified working
for every command in this task.
2. **Credentials** — run `aws sts get-caller-identity --region <REGION>`. Pass `--region` so
STS uses the regional endpoint (`sts.<region>.amazonaws.com`); the global endpoint
(`sts.amazonaws.com`) is legacy and best avoided. If it fails, ask the user how they
authenticate. Prefer assuming an IAM role with ephemeral credentials over long-lived access keys (named profile / IAM Identity Center (SSO) / access keys), configure accordingly, and from then on
append `--profile <PROFILE>` to EVERY command if a profile is in play. Enable AWS CloudTrail in
the account so all `pinpoint-sms-voice-v2` API calls (notably `SendNotifyTextMessage`) are
logged — OTP flows are abuse-sensitive (toll fraud, OTP pumping) and need an audit trail.
3. **Service access** — read-only check:
`aws pinpoint-sms-voice-v2 describe-notify-configurations --region <REGION>`
Minimum IAM actions for this task: `sms-voice:CreateNotifyConfiguration`,
`DescribeNotifyConfigurations`, `DescribeNotifyTemplates`, `ListNotifyCountries`,
`SendNotifyTextMessage`, and `DeleteNotifyConfiguration` (for cleanup).
4. **Input validation** — validate display name (≤15 chars, allowed characters), phone (E.164), and country before mutating APIs.
5. **Inputs** — ask the user for: a display name (their brand or app name, 15 chars max — this
is what recipients may see, and it goes through an automated brand review; a made-up brand
is fine for testing, but a well-known third-party brand name will be flagged), their phone
number in E.164, and the destination country if not obvious from the number.
## Steps
### 1. Create the Notify configuration
```bash
aws pinpoint-sms-voice-v2 create-notify-configuration \
--display-name "<DISPLAY_NAME>" \
--use-case CODE_VERIFICATION \
--enabled-channels SMS \
--region <REGION>
```
Save `NotifyConfigurationId` as `NC_ID`. (`CODE_VERIFICATION` is the only use case Notify
supports.)
### 2. Poll until ACTIVE
Most configurations activate within seconds. Only `ACTIVE` can send.
```bash
aws pinpoint-sms-voice-v2 describe-notify-configurations \
--notify-configuration-ids <NC_ID> --region <REGION> \
--query 'NotifyConfigurations[0].Status'
```
- `PENDING` → wait a few seconds and re-poll (control-plane APIs are rate-limited to ~1
request/second — do not poll faster than every 2–3 seconds).
- `ACTIVE` → proceed.
- `REQUIRES_VERIFICATION` or `REJECTED` → see Failure modes.
### 3. Pick a template
You cannot write your own message — Notify uses AWS-managed, pre-approved templates. List the
SMS templates available on the Basic tier:
```bash
aws pinpoint-sms-voice-v2 describe-notify-templates \
--filters '[{"Name":"channels","Values":["SMS"]},{"Name":"tier-access","Values":["BASIC"]}]' \
--region <REGION>
```
The unfiltered payload is very large (every template lists all supported countries) — add a
`language-code` filter to keep it manageable, e.g. append
`{"Name":"language-code","Values":["en"]}` to the filters array — values are lowercase ISO
codes (`en`, `es`, `fr-CA`, ...); uppercase fails with `INVALID_FILTER_VALUES`. Save the chosen `TemplateId`
as `TEMPLATE_ID`, and read its variable definitions: variables marked `CUSTOMER` you supply;
variables marked `SYSTEM` are filled automatically — notably `brandName`, which comes from
your configuration's display name, so you do NOT pass it. Most English code-verification
templates take only `code` (must match `^\d{4,8}$`), but some variants require additional
variables (e.g. `time`) — read the `Variables` list of the specific template you pick rather
than assuming; a missing required variable fails the send with `ValidationException`.
### 4. Confirm the destination country is enabled (cheap, avoids a failed send)
```bash
aws pinpoint-sms-voice-v2 list-notify-countries \
--channels SMS --tier BASIC --region <REGION>
```
The Basic tier covers a curated set of countries — the `list-notify-countries` output above
shows the current list. If the user's country is not listed, tell them — the fix is the Advanced tier (see Next steps), not a
different command.
### 5. Dry run, then send
Generate a 6-digit code (this is a demo — in production your app generates and stores it).
First validate everything without sending or charging:
```bash
aws pinpoint-sms-voice-v2 send-notify-text-message \
--notify-configuration-id <NC_ID> \
--destination-phone-number "<PHONE>" \
--template-id <TEMPLATE_ID> \
--template-variables '{"code":"482913"}' \
--dry-run \
--region <REGION>
```
(All template variable values are passed as strings, even numbers and booleans.)
> **Production note:** In production, pass template variables via `--cli-input-json` from a
> file rather than inline to avoid shell history exposure. Ensure CloudTrail logs are encrypted
> with a KMS CMK since they will contain OTP codes.
A passing dry run returns the same shape as a real send — `MessageId` plus
`ResolvedMessageBody` (the rendered text) — with nothing transmitted or charged. If the dry
run passes, confirm with the user — "the next command sends a real text to `<PHONE>`, ready?" —
then send for real by repeating the command **without** `--dry-run`. If the user only wanted
validation, stop here: the dry run already proved the configuration end to end.
Save `MessageId`. The response also contains `ResolvedMessageBody` — the exact rendered text.
Show it to the user and ask: "This should be on your phone now — did it arrive?"
Treat `ResolvedMessageBody` as sensitive — it contains the OTP code. Do not log or persist it beyond this confirmation.
## Verify
- The user confirms the OTP text arrived and the code matches.
- `ResolvedMessageBody` in the response shows the rendered template.
Print a summary: display name, `NC_ID`, `TEMPLATE_ID`, region, and the daily-limit reminder
below.
**Testing note**: Notify enforces a non-adjustable per-day cap per destination number, applied at both the configuration and account level — check the applicable value in the Service Quotas console or documentation. Don't burn the day's budget on retries.
## Next steps
- **Production hardening for OTP**: set a default template
(`update-notify-configuration --default-template-id <TEMPLATE_ID>`), route delivery events to
your systems via a configuration set (the configuration-sets-and-event-destinations guide),
and read the Notify compliance prerequisites (terms, privacy policy, opt-in) before real traffic.
- **Observability (required before production)**: Enable CloudTrail logging and attach a
configuration set with an event destination (SNS with SSE/KMS, `aws:SourceArn`/`aws:SourceAccount` condition keys, and authorized subscriptions only; CloudWatch Logs encrypted with a
KMS CMK, or Kinesis Firehose with server-side encryption) for delivery event
monitoring. Create CloudWatch alarms on send failure rates and spend
approaching the daily cap to detect misuse early.
- **Rate-limit OTP verification**: Implement application-side rate limiting on verification
attempts (max 3–5 per code, lockout after failures) to prevent brute-force attacks.
- **Rate-limit OTP sends**: Implement application-side throttling on send requests (max 1 OTP per
phone per 60s, progressive backoff) to prevent SMS pumping and toll fraud.
- **Voice OTP**: add `VOICE` to `--enabled-channels`, then `send-notify-voice-message ...
--voice-id JOANNA`. Format the code as `"4. 8. 2. 9. 1. 3."` so text-to-speech reads digits
individually.
- **Higher volume / more countries**: upgrade Basic → Advanced (higher daily limits, higher TPS, broader country coverage — see
applicable limits in the AWS End User Messaging Notify documentation) via a tier-upgrade registration with opt-in proof; review timeline
per the [ETA page](https://docs.aws.amazon.com/sms-voice/latest/userguide/registration-eta.html): see [references/customer-go-to-production-guide.md](customer-go-to-production-guide.md) (Path C), which discovers the exact registration type live. This is OPTIONAL — Basic is
production-usable within its limits. Countries that additionally need your own identity are
live data — see the registration-requirements-by-country guide.
- **Beyond OTP** — conversational and rich messaging needs your own origination identity:
start with `references/hello-rcs-test-agent.md` or [references/customer-go-to-production-guide.md](customer-go-to-production-guide.md).
## Security Considerations
- **Ephemeral credentials**: Prefer IAM roles over long-lived access keys.
- **OTP codes in logs**: CloudTrail and downstream logs contain OTP codes; encrypt with KMS CMK.
- **Rate limiting**: Throttle both send requests and verification attempts at the application layer.
- **Event destination encryption**: Use SSE/KMS on SNS topics, encrypt CloudWatch Logs, restrict subscriptions.
- **Input validation**: Validate display name, phone (E.164), and country before mutating APIs.
## Failure modes
| Error / symptom | Cause | Fix |
|---|---|---|
| `AccessDeniedException` | Missing IAM actions or expired session | Re-auth; attach the IAM actions from Prerequisites step 3 |
| Status stays `PENDING` | Validation in progress | Re-poll every few seconds; escalate only after several minutes |
| Status `REQUIRES_VERIFICATION` | Brand/display name needs verification (third-party brand names need a letter of authorization ≤30 days old, via a Support case) | Easiest for a demo: create a new configuration with a generic display name |
| Status `REJECTED` | Automated review rejected the display name (profanity, URLs, impersonation) | Read `RejectionReason` in describe output; recreate with a compliant name |
| `ConflictException` on send | Configuration not ACTIVE | Wait for ACTIVE (step 2) |
| `ValidationException` on send | Missing/invalid template variable, malformed E.164, or destination country not enabled | Check required variables against step 3's definitions; re-verify phone format; re-run step 4 |
| `ResourceNotFoundException` | Wrong `NC_ID` or `TEMPLATE_ID` | Re-list and re-save the IDs |
| `ServiceQuotaExceededException` | Daily limit (per-destination and account daily caps (check applicable limits via Service Quotas)) or spend limit hit | Wait for the daily reset; for spend, `SetNotifyMessageSpendLimitOverride` (account-level change — confirm with the user; Notify's override is separate from SMS) |
| Send succeeds but nothing arrives | Carrier filtering or wrong number | Confirm the number; retry once; check delivery events if a configuration set was attached |
| User replies STOP and worries | On AWS-managed identities STOP is informational only for OTP — each new OTP request is an implicit opt-in | No action needed; explain the semantics |
## Cleanup (destructive — confirm with the user first)
```bash
aws pinpoint-sms-voice-v2 delete-notify-configuration \
--notify-configuration-id <NC_ID> --region <REGION>
```
references/hello-rcs-test-agent.md
# Hello AWS End User Messaging: Create an RCS test agent and send your first message
> By the end of this task, a branded RCS agent exists in the user's AWS account, their phone is a
> verified test device, and they have received a rich, branded message from the agent — plus sent
> one back. About 5 minutes of working time; the tester invitation adds a 2–20 minute wait that
> happens in the background. No carrier registration, no phone number purchase, no infrastructure.
> This unlocks the entire SMS and RCS control and data plane for everything you build next.
You run every command yourself. The user provides inputs and confirms what they see on their
phone. Stop and consult the Failure modes table on any error.
## Session state
Track these values as you collect them. They carry across all steps.
| Value | Format | Source |
|---|---|---|
| `REGION` | e.g. `us-east-1` | ask the user (default `us-east-1`) |
| `PROFILE` | AWS CLI profile name | credential bootstrap (may be empty) |
| `BRAND_NAME` | 2–65 chars | ask the user |
| `AGENT_ID` | `rcs-` + 32 hex | CreateRcsAgent response |
| `REG_ID` | `registration-...` | CreateRegistration response |
| `LOGO_ID`, `BANNER_ID` | `attachment-...` (ID prefix is `attachment-` even though the parameter is `--registration-attachment-id`) | CreateRegistrationAttachment responses |
| `PHONE` | E.164, e.g. `+12065550123` | ask the user (their real test device) |
| `VDN_ID` | `vdn-` + 32 hex | CreateVerifiedDestinationNumber response (needed for cleanup) |
## Prerequisites
1. **AWS CLI capability — probe, don't trust the version.** A current-looking CLI can bundle
a stale or partial service model that predates some `pinpoint-sms-voice-v2` operations
(observed live on CLI v2.33.15: `Invalid choice 'create-rcs-agent'`). No single probe is
authoritative — a partial model can include some operations while omitting others. Treat any
`Invalid choice '<operation>'` at ANY step as a stale/partial model and resolve it per
prerequisite 3 (check for a stale `~/.aws/models/` override first, otherwise give the user the
AWS CLI v2 upgrade commands; boto3 fallback for locked environments). Separately note v1 vs v2:
v1 changes the URL-field behavior in the registration steps (handled where it matters below).
2. **Credentials** — run `aws sts get-caller-identity --region <REGION>`. Pass `--region` so
STS uses the regional endpoint (`sts.<region>.amazonaws.com`); the global endpoint
(`sts.amazonaws.com`) is legacy and best avoided. If it fails, ask the user how they
authenticate and follow the matching branch:
- **Named profile**: ask for the profile name; from now on append `--profile <PROFILE>` to
EVERY aws command in this task. Missing it on even one command causes
`NoCredentials`/`ExpiredTokenException`.
- **IAM Identity Center (SSO)**: `aws sso login --profile <PROFILE>`, then treat as named profile.
- **Access keys**: `aws configure` (or `aws configure --profile <PROFILE>`).
Prefer IAM Identity Center (SSO) or assumed-role ephemeral credentials over long-lived access keys.
If access keys are used, rotate them regularly and never embed them in code.
Re-run `aws sts get-caller-identity --region <REGION>` until it succeeds. Ensure AWS CloudTrail
is enabled in the account so all `pinpoint-sms-voice-v2` API calls (agent creation, registration
submission, message sends) are logged for auditing and abuse detection.
3. **Service access AND RCS capability in one probe** — `describe-spend-limits` is the WRONG
canary (it exists in old CLIs too). Probe with an RCS-specific read-only call:
```bash
aws pinpoint-sms-voice-v2 describe-rcs-agents --region <REGION> >/dev/null && echo "RCS APIs: OK"
```
- `AccessDeniedException` → the identity needs the required `sms-voice:` permissions,
scoped to the specific actions these steps use rather than the `sms-voice:*` wildcard (see Failure modes).
- `Invalid choice '<operation>'` on this probe OR on ANY `pinpoint-sms-voice-v2` operation at
any later step (a stale or partial local model can carry some operations while omitting
others, e.g. the RCS agent control-plane present but `send-rcs-message` absent) → the local
client's service model is stale or partial, not a permissions or account problem. Resolve it:
1. **Check for a stale local model override first** — a leftover file shadows the SDK's
bundled model, so even a current CLI stays broken:
```bash
ls ~/.aws/models/pinpoint-sms-voice-v2/
```
If present, rename it away (e.g. `mv ~/.aws/models/pinpoint-sms-voice-v2 ~/.aws/models/pinpoint-sms-voice-v2.archive`) and retry the operation.
2. **Otherwise the installed CLI is too old** — tell the user their AWS CLI predates this
operation and **give them the commands to upgrade** to the latest AWS CLI v2 (run
`aws --version`, then follow the
[AWS CLI install/update guide](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html)).
You do NOT need to determine which CLI version introduced the operation — upgrading to
the latest v2 and retrying is sufficient; don't research CLI version history.
Do not run the upgrade yourself — a CLI upgrade is a privileged system change the user
should perform. Retry the operation after upgrading.
3. **Locked environment that cannot upgrade** → boto3 fallback (it often carries the
operations even when the CLI does not). Probe for the SPECIFIC operation that failed,
expressed in boto3 PascalCase (e.g. `create-rcs-agent` → `CreateRcsAgent`,
`send-rcs-message` → `SendRcsMessage`) — do not assume one operation's presence implies
another's, since a partial model can carry some while omitting others:
```bash
python3 -c "import boto3; c=boto3.client('pinpoint-sms-voice-v2', region_name='<REGION>'); print('<PascalCaseOp>' in c.meta.service_model.operation_names)"
```
`True` → run the RCS-agent steps through boto3 (parameters are PascalCase:
`RegistrationId`, `SelectChoices=[...]`, `AttachmentBody=<bytes>` — vs the CLI's
`--kebab-case`). `False` → `pip install -U boto3` (AWS software) and re-check; if STILL
false, apply the same stale-override check to `~/.aws/models/` and rename it away.
Any resources already created before the failure (agent, registration, verified tester) remain
valid — once the client is fixed, resume the flow; nothing needs recreating.
4. **Image tooling — nothing to install.** If you will GENERATE brand assets (step 1, option B),
the primary path is a Python-stdlib PNG generator (zlib + struct — already present with
`python3`). Optionally run `which rsvg-convert`: if it is ALREADY installed you may render a
nicer SVG design instead, but do NOT install it — the stdlib generator is sufficient for this
quickstart. Skip this check if the user provides their own images.
5. **The user's phone** — an Android phone with RCS enabled, or an iPhone on iOS 18+, with the
number in `PHONE`. Confirm the user has it in hand; they will tap an invitation later.
6. **Input validation** — validate user-provided values (E.164 for phones, URL scheme allowlist, length checks) before passing to APIs.
7. **Inputs to collect** — ask the user:
- Brand name.
- **Registration details.** By default you fill these with synthetic placeholder values
(`.example.com` contacts, an invented description, a safe accent color). Do NOT open by
interviewing the user for each field or by running live field discovery to decide what to
ask — go straight to the synthetic defaults. Be explicit that the
data is synthetic: **show the user the full set of values you will use and let them opt out** —
"these are placeholder values for a test agent; tell me if you'd rather provide your own for
any field." Get their confirmation (or their replacements) before submitting; never submit
invented data silently. If the user supplies real values (contact names, emails, phone
numbers, business details), treat them as sensitive business-identifying PII: use them only
to populate the registration and to confirm back, and do not repeat them in responses or
write them to logs beyond what confirmation requires.
- **Do they have their own logo and banner images?** If yes, get the file paths — you will
validate them in step 1 (option A). If no, generate placeholder assets (option B, quick mode),
which fall under the same synthetic-data disclosure above.
## Steps
### 1. Brand assets — validate the user's own, or generate them
The registration requires a logo (query dimensions from `describe-registration-field-definitions`; expected 224×224 px, under 50 KB, PNG or JPEG) and a banner
(query dimensions from `describe-registration-field-definitions`; expected 1440×448 px, under 200 KB, PNG or JPEG). The registration API rejects anything outside
these constraints, so verify BEFORE uploading either way. Both images are uploaded with
`create-registration-attachment` and then referenced by their returned attachment IDs in the
`agentDetails.logoImage` and `agentDetails.bannerImage` registration field values (see the
attachment upload and field-value steps below).
#### Option A — the user provides their own images
Copy their files to `brand-assets/logo.png` and `brand-assets/banner.png` (or `.jpg`), then
validate format, dimensions, and size with this stdlib-only check:
```bash
# Dimensions per describe-registration-field-definitions (Step 5).
python3 - brand-assets/logo.png 224 224 51200 <<'EOF' && echo "logo OK"
import struct, sys
path, w, h, maxb = sys.argv[1], int(sys.argv[2]), int(sys.argv[3]), int(sys.argv[4])
data = open(path, 'rb').read()
if len(data) > maxb: sys.exit(f"FAIL: {len(data)} bytes exceeds {maxb}")
if data[:8] == b'\x89PNG\r\n\x1a\n':
iw, ih = struct.unpack('>II', data[16:24])
elif data[:2] == b'\xff\xd8':
i, iw, ih = 2, None, None
while i < len(data) - 9:
if data[i] != 0xFF: i += 1; continue
m = data[i+1]
if 0xC0 <= m <= 0xCF and m not in (0xC4, 0xC8, 0xCC):
ih, iw = struct.unpack('>HH', data[i+5:i+9]); break
i += 2 + struct.unpack('>H', data[i+2:i+4])[0]
if iw is None: sys.exit("FAIL: could not parse JPEG dimensions")
else:
sys.exit("FAIL: not PNG or JPEG (SVG and other formats are rejected)")
sys.exit(0 if (iw, ih) == (w, h) else f"FAIL: {iw}x{ih}, need exactly {w}x{h}")
EOF
python3 - brand-assets/banner.png 1440 448 204800 <<'EOF' && echo "banner OK"
import struct, sys
path, w, h, maxb = sys.argv[1], int(sys.argv[2]), int(sys.argv[3]), int(sys.argv[4])
data = open(path, 'rb').read()
if len(data) > maxb: sys.exit(f"FAIL: {len(data)} bytes exceeds {maxb}")
if data[:8] == b'\x89PNG\r\n\x1a\n':
iw, ih = struct.unpack('>II', data[16:24])
elif data[:2] == b'\xff\xd8':
i, iw, ih = 2, None, None
while i < len(data) - 9:
if data[i] != 0xFF: i += 1; continue
m = data[i+1]
if 0xC0 <= m <= 0xCF and m not in (0xC4, 0xC8, 0xCC):
ih, iw = struct.unpack('>HH', data[i+5:i+9]); break
i += 2 + struct.unpack('>H', data[i+2:i+4])[0]
if iw is None: sys.exit("FAIL: could not parse JPEG dimensions")
else:
sys.exit("FAIL: not PNG or JPEG (SVG and other formats are rejected)")
sys.exit(0 if (iw, ih) == (w, h) else f"FAIL: {iw}x{ih}, need exactly {w}x{h}")
EOF
```
If a check FAILs, tell the user exactly what's wrong and offer fixes:
- **Wrong dimensions** — resize (do not stretch a mismatched aspect ratio without asking; offer
to pad instead): with rsvg-convert unavailable, `sips -z 224 224 logo.png` works on macOS, or
regenerate at the right size from their source file.
- **Too large** — re-export as PNG with fewer colors, or as JPEG quality ~85.
- **Wrong format** (SVG, WebP, HEIC...) — convert to PNG first.
Also pick the accent color to complement their images (ask, or sample a dominant dark color) —
the contrast rule below still applies.
#### Option B — generate the assets
Work in a fresh directory (e.g. `mkdir -p ~/hello-eum-<brand-slug> && cd` into it) — a shared
`brand-assets/` dir may hold leftovers from previous runs for a different brand; don't reuse
or blindly overwrite files you didn't create this run without checking them first.
Pick an accent color with at least **4.5:1 contrast against white**. Safe choices:
`#0D47A1` `#1B5E20` `#BF360C` `#B71C1C` `#4A148C`. Never light or pastel colors — the review
rejects them with `ACCENT_COLOR_CONTRAST_INSUFFICIENT`.
Generate both images with this stdlib-only script (nothing to install — zlib + struct only):
a solid accent-color background with simple white geometry (centered disc on the logo, a
horizontal band on the banner). Substitute the accent hex you picked:
```bash
mkdir -p brand-assets
python3 - brand-assets "<ACCENT_HEX>" <<'EOF'
import os, struct, sys, zlib
outdir, accent = sys.argv[1], sys.argv[2].lstrip('#')
bg = tuple(int(accent[i:i + 2], 16) for i in (0, 2, 4))
fg = (255, 255, 255)
def chunk(tag, data):
return (struct.pack('>I', len(data)) + tag + data
+ struct.pack('>I', zlib.crc32(tag + data) & 0xFFFFFFFF))
def write_png(path, w, h, pixel):
rows = bytearray()
for y in range(h):
rows.append(0) # PNG filter type None for this scanline
for x in range(w):
rows += bytes(pixel(x, y))
ihdr = struct.pack('>IIBBBBB', w, h, 8, 2, 0, 0, 0)
with open(path, 'wb') as f:
f.write(b'\x89PNG\r\n\x1a\n' + chunk(b'IHDR', ihdr)
+ chunk(b'IDAT', zlib.compress(bytes(rows), 9)) + chunk(b'IEND', b''))
print(f'{path}: {w}x{h}, {os.path.getsize(path)} bytes')
# Dimensions per describe-registration-field-definitions (Step 5).
write_png(os.path.join(outdir, 'logo.png'), 224, 224,
lambda x, y: fg if (x - 112) ** 2 + (y - 112) ** 2 <= 64 ** 2 else bg)
write_png(os.path.join(outdir, 'banner.png'), 1440, 448,
lambda x, y: fg if 200 <= y < 248 else bg)
EOF
ls -la brand-assets/*.png
```
Middle tier — **if Pillow is already installed** (`python3 -c "import PIL"` succeeds; do NOT
install it), it renders proper brand-name text, which the stdlib generator cannot. Two traps
observed live: font paths vary by distro — never hardcode; discover with
`fc-list : file | grep -iE "sans|noto|dejavu" | head` and pass a discovered `.ttf` to
`ImageFont.truetype(path, size)`. And if the font fails to load, Pillow silently falls back to
a tiny bitmap font — the banner text renders tiny and off-scale; validate visually or check
the font loaded before trusting the output.
IF `rsvg-convert` is already installed (`which rsvg-convert` from the prerequisites), you may
instead write logo/banner SVGs (solid accent-color background, a basic shape icon, brand name
text, Arial font) and render them for a nicer design:
`rsvg-convert -w 224 -h 224 brand-assets/logo.svg -o brand-assets/logo.png` and
`rsvg-convert -w 1440 -h 448 brand-assets/banner.svg -o brand-assets/banner.png`. Do NOT
install it — the stdlib generator above is sufficient for the quickstart.
Checkpoint (both options): files exist, logo under 50 KB, banner under 200 KB, dimensions exact.
Generated assets can be validated with the option A script too. If a generated file is over the
size limit, simplify the design (fewer distinct colors/shapes) and regenerate.
### 2. Create the agent
The agent is an empty container — its name, colors, and images all come from the registration
in the next steps, not from this call.
```bash
aws pinpoint-sms-voice-v2 create-rcs-agent \
--deletion-protection-enabled \
--region <REGION>
```
Save `RcsAgentId` as `AGENT_ID`.
### 3. Create the test registration and link it to the agent
```bash
aws pinpoint-sms-voice-v2 create-registration \
--registration-type TEST_RCS_LAUNCH_REGISTRATION \
--region <REGION>
```
Save `RegistrationId` as `REG_ID`. Link it (this must happen before submitting):
```bash
aws pinpoint-sms-voice-v2 create-registration-association \
--registration-id <REG_ID> \
--resource-id <AGENT_ID> \
--region <REGION>
```
### 4. Upload the brand images
`--attachment-body` and `--attachment-url` cannot be combined; use `--attachment-body` with a
local `fileb://` path:
```bash
aws pinpoint-sms-voice-v2 create-registration-attachment \
--attachment-body fileb://brand-assets/logo.png --region <REGION>
```
Save `RegistrationAttachmentId` as `LOGO_ID`. Repeat for `banner.png`, save as `BANNER_ID`.
### 5. Fill every registration field
Each field has a type that determines the CLI parameter. Using the wrong one fails:
| Field type | CLI parameter |
|---|---|
| TEXT | `--text-value "<value>"` |
| SELECT | `--select-choices "<value>"` |
| ATTACHMENT | `--registration-attachment-id "<id>"` |
(`--field-values` does not exist. Do not invent it.)
**Query the field definitions FIRST and drive your field-setting from that output** — the
table below is illustrative defaults, not the source of truth (field sets and SELECT choices
can drift by service version):
```bash
aws pinpoint-sms-voice-v2 describe-registration-field-definitions \
--registration-type TEST_RCS_LAUNCH_REGISTRATION --region <REGION>
```
Inspect the full response — each `FieldDefinition` object contains the field requirement,
type, path, validation constraints, and any conditional dependencies. Do not filter this
output; conditional field requirements depend on context from the complete definition shape.
SELECT options live under `SelectValidation.Options` as a list of plain strings (not objects) —
a parser expecting `{OptionName: ...}` will throw. If any command below fails on an unknown
field or the submit step reports a missing one, the definitions output wins.
Set every TEXT field (one `put-registration-field-value` call each, same shape):
```bash
aws pinpoint-sms-voice-v2 put-registration-field-value --registration-id <REG_ID> \
--field-path "agentDetails.brandName" --text-value "<BRAND_NAME>" --region <REGION>
```
Repeat for each row (quick-mode defaults shown). If you batch these with a shell loop or
function, inline `--region` (and `--profile`) literally in the command — passing multi-word
flag strings through a variable (`R="--region us-east-1"`) breaks on word-splitting:
| FieldPath | Value | Constraint |
|---|---|---|
| `agentDetails.brandName` | brand name | 2–65 chars |
| `agentDetails.serviceName` | `<BRAND_NAME> RCS Agent` | 1–100 chars |
| `agentDetails.senderDisplayName` | brand name | 1–40 chars |
| `agentDetails.agentDescription` | one-liner you invent | **max 100 chars** |
| `agentDetails.accentColor` | the hex from step 1 | `#RRGGBB`, 4.5:1 vs white |
| `agentDetails.contactPhoneNumber` | `+12065550100` | E.164 |
| `agentDetails.contactPhoneLabel` | `Call Us` | 1–25 chars |
| `agentDetails.contactEmailAddress` | `hello@<brand-slug>.example.com` | email |
| `agentDetails.contactEmailLabel` | `Email Us` | 1–25 chars |
| `agentDetails.contactWebsite` | `https://www.<brand-slug>.example.com` | URL |
| `agentDetails.contactWebsiteLabel` | `Visit Website` | 0–25 chars |
| `agentDetails.privacyPolicyUrl` | `https://www.example.com/privacy` | URL |
| `agentDetails.privacyPolicyLabel` | `Privacy Policy` | 0–25 chars |
| `agentDetails.termsAndConditionsUrl` | `https://www.example.com/terms` | URL |
| `agentDetails.termsAndConditionsLabel` | `Terms and Conditions` | 0–25 chars |
| `agentDetails.monthlyRcsVolume` | `1000` | 1–6 digits |
| `complianceKeywords.helpResponse` | `Reply STOP to opt out. For help, contact <email>` | 1–160 chars |
| `complianceKeywords.stopResponse` | `You have been unsubscribed. No more messages will be sent.` | 1–160 chars |
Note on the three URL fields (`contactWebsite`, `privacyPolicyUrl`, `termsAndConditionsUrl`):
on **AWS CLI v2** the plain `--text-value "https://..."` form works. On **AWS CLI v1**, a legacy
"paramfile" feature fetches `https://` parameter values as remote files, so the command fails —
either client-side ("Unable to retrieve ... parameter file" / usage error) or server-side
(`ValidationException: INVALID_PARAMETER Fields="textValue"` when the CLI fetched the page and
sent its HTML), depending on whether the URL resolves. If you see that error (or you
detected CLI v1 in prerequisites), use the JSON form, which bypasses paramfile:
```bash
aws pinpoint-sms-voice-v2 put-registration-field-value --region <REGION> --cli-input-json \
'{"RegistrationId":"<REG_ID>","FieldPath":"agentDetails.contactWebsite","TextValue":"https://www.example.com"}'
```
Set the SELECT fields (note `--select-choices`, not `--text-value`):
```bash
aws pinpoint-sms-voice-v2 put-registration-field-value --registration-id <REG_ID> \
--field-path "agentDetails.useCase" --select-choices "MULTI_USE" --region <REGION>
aws pinpoint-sms-voice-v2 put-registration-field-value --registration-id <REG_ID> \
--field-path "agentDetails.averageMonthlyRcsFrequency" --select-choices "10" --region <REGION>
```
Set the remaining SELECT field — easy to miss because older references omit it:
```bash
aws pinpoint-sms-voice-v2 put-registration-field-value --registration-id <REG_ID> \
--field-path "agentDetails.billingCategory" --select-choices "CONVERSATIONAL" --region <REGION>
```
Set the ATTACHMENT fields:
```bash
aws pinpoint-sms-voice-v2 put-registration-field-value --registration-id <REG_ID> \
--field-path "agentDetails.logoImage" --registration-attachment-id "<LOGO_ID>" --region <REGION>
aws pinpoint-sms-voice-v2 put-registration-field-value --registration-id <REG_ID> \
--field-path "agentDetails.bannerImage" --registration-attachment-id "<BANNER_ID>" --region <REGION>
```
### 6. Audit, then submit and poll for approval
Do not trust your own count of fields set — audit against the service before submitting.
Compare what is SET against what is REQUIRED:
```bash
aws pinpoint-sms-voice-v2 describe-registration-field-values \
--registration-id <REG_ID> --region <REGION> \
--query 'RegistrationFieldValues[].FieldPath' --output text | tr '\t' '\n' | sort > /tmp/set.txt
aws pinpoint-sms-voice-v2 describe-registration-field-definitions \
--registration-type TEST_RCS_LAUNCH_REGISTRATION --region <REGION> \
--query "RegistrationFieldDefinitions[].FieldPath" --output text | tr '\t' '\n' | sort > /tmp/req.txt
comm -13 /tmp/set.txt /tmp/req.txt
```
Do not hardcode a filter on the requirement value — derive the field set from the full
definition and let the live schema decide applicability. Any line printed is a defined field you
have not set; confirm from the field's own definition whether it applies before continuing.
Then submit:
```bash
aws pinpoint-sms-voice-v2 submit-registration-version \
--registration-id <REG_ID> --region <REGION>
```
Poll BOTH statuses every 15 seconds — they move independently:
```bash
aws pinpoint-sms-voice-v2 describe-registrations --registration-ids <REG_ID> \
--query 'Registrations[0].RegistrationStatus' --region <REGION>
aws pinpoint-sms-voice-v2 describe-rcs-agents \
--query "RcsAgents[?RcsAgentId=='<AGENT_ID>'].TestingAgent.Status" --region <REGION>
```
Expected: registration `SUBMITTED → REVIEWING → COMPLETE`, and `TestingAgent.Status: ACTIVE`.
Typically under 5 minutes; don't worry before 10. If the registration lands in
`REQUIRES_UPDATES`, see Failure modes — do not create a new version blindly.
Checkpoint: tell the user the agent is live. To let them **visually see the branding they just
configured** (logo, banner, accent color, contact details as they render), give them the
console link to the agent page:
`https://<REGION>.console.aws.amazon.com/sms-voice/home?region=<REGION>#/rcs-agents?agent-id=<AGENT_ID>`
If they are not already signed in, tell them to sign in to the AWS console first, then open the
link. (This is view-only confirmation; no changes are needed here.)
### 7. Add the user's phone as a verified tester
Wait at least **120 seconds after agent creation** before this call, or it can fail silently.
```bash
aws pinpoint-sms-voice-v2 create-verified-destination-number \
--destination-phone-number <PHONE> \
--rcs-agent-id <AGENT_ID> \
--region <REGION>
```
Tell the user, verbatim enough that they can act on it:
- "You'll get a tester invitation on your phone in 2–20 minutes, from a sender called
**RBM Tester Management**."
- "On iPhone, it may land in the **Unknown Senders** folder in Messages."
- "Tap **Make me a tester** when it arrives, then tell me."
While waiting, verify the invitation went out:
```bash
aws pinpoint-sms-voice-v2 describe-verified-destination-numbers \
--filters Name=rcs-agent-id,Values=<AGENT_ID> --region <REGION> \
--query 'VerifiedDestinationNumbers[].{Phone:DestinationPhoneNumber,Status:Status}'
```
Wait for the user to confirm they tapped it (status becomes `VERIFIED`).
### 8. Clear send blockers, then send the first message
Two account-level settings can block the send. Check both first:
```bash
# Is the destination country blocked by a protect configuration?
aws pinpoint-sms-voice-v2 describe-protect-configurations --region <REGION>
# Is the phone on the opt-out list (e.g. from a previous STOP)?
aws pinpoint-sms-voice-v2 describe-opted-out-numbers \
--opt-out-list-name Default --region <REGION>
```
Interpreting the protect output: a protect configuration only applies to this send if it is
the **account default** (`AccountDefault: true`), associated with a configuration set you pass
on the send, or passed explicitly as `ProtectConfigurationId`. Other protect configurations in
the account are inert for this quickstart — do not "fix" them.
Fixes, only if needed (each modifies **account-wide** state — confirm with the user first).
⚠️ These are not scoped to this agent or send: a protect-configuration country-rule change and
an opt-out-list deletion affect **every origination identity in the account** (all agents, phone
numbers, and pools), and opt-out removal re-enables messaging to a number that previously sent
STOP. Change them only when you own the account-level policy and understand the blast radius:
- Country blocked: `aws pinpoint-sms-voice-v2 update-protect-configuration-country-rule-set --protect-configuration-id <ID> --number-capability SMS --country-rule-set-updates '{"US":{"ProtectStatus":"ALLOW"}}' --region <REGION>` (the SMS capability governs RCS sends too)
- Opted out: `aws pinpoint-sms-voice-v2 delete-opted-out-number --opt-out-list-name Default --opted-out-number <PHONE> --region <REGION>`
Send:
```bash
aws pinpoint-sms-voice-v2 send-rcs-message \
--destination-phone-number <PHONE> \
--origination-identity <AGENT_ID> \
--rcs-message-content '{"Content":{"TextMessage":{"Body":"Hello from <BRAND_NAME>! This is your first RCS message."}}}' \
--message-traffic-type TRANSACTIONAL \
--region <REGION>
```
> The message text goes in `--rcs-message-content` as JSON (`Content.TextMessage.Body`); there is
> no `--message-body` flag. `--message-traffic-type` takes `TRANSACTIONAL` or `PROMOTIONAL`.
>
> **Confirm the current shape against your CLI, don't trust these flags blindly.** The
> `pinpoint-sms-voice-v2` request shape has changed across versions. Before sending, generate the
> skeleton from the installed CLI and fill it in, so you use whatever shape *your* CLI expects
> rather than a possibly-stale example:
>
> ```bash
> aws pinpoint-sms-voice-v2 send-rcs-message --generate-cli-skeleton input > send-rcs.json
> # edit send-rcs.json (DestinationPhoneNumber, OriginationIdentity,
> # RcsMessageContent.Content.TextMessage.Body, MessageTrafficType), then:
> aws pinpoint-sms-voice-v2 send-rcs-message --cli-input-json file://send-rcs.json --region <REGION>
> ```
>
> If a documented flag is rejected (`Unknown options` / `Invalid choice`), regenerate the skeleton
> and use its fields — this is the same reactive stale-model handling as prerequisite 3, applied to
> parameter shape instead of operation existence.
<!-- -->
> **Production note:** For production, add `--configuration-set-name <CONFIG_SET_NAME>` to
> enable delivery telemetry (create a configuration set with an event destination first).
> Pass message bodies via `--cli-input-json` from a file to avoid sensitive content
> persisting in shell history.
Save `MessageId` — and know what it means: **`MessageId` proves the service ACCEPTED the
message, not that it was delivered.** This quickstart sends without a configuration set, which
means there is NO delivery telemetry — you cannot look up what happened to a message
downstream. Set delivery expectations accordingly:
1. Ask the user: "Check your phone — you should see a branded message from <BRAND_NAME> with
your logo. On iPhone, check Unknown Senders. Give it up to 2 minutes."
2. If nothing arrives: **resend the same command once** before any deeper diagnosis. This is
the cheapest test and frequently resolves it.
3. If the resend also doesn't arrive, check the device, not the account (the API accepted
both sends): Android — Messages → Settings → RCS chats → status must be "Connected";
iPhone — Settings → Apps → Messages → RCS Messaging enabled (iOS 18+, carrier support).
4. Do NOT build logging infrastructure mid-quickstart — but enabling CloudTrail and a
configuration set with event destinations (SNS with SSE/KMS, CloudWatch Logs with CMK,
or Kinesis Firehose with server-side encryption) is REQUIRED before production use.
Encrypt CloudTrail logs and CloudWatch Log groups with a KMS CMK, as message
bodies and delivery metadata may contain sensitive information.
Create CloudWatch alarms on send failure rates, throttling events, and spend
limit utilization to detect anomalous activity before it impacts production. If the user wants real delivery
telemetry, that is a configuration set + event destination — finish this task first, then
follow the configuration-sets-and-event-destinations guide.
### 9. Prove inbound works — no infrastructure needed
Set up an auto-response keyword:
```bash
aws pinpoint-sms-voice-v2 put-keyword \
--origination-identity <AGENT_ID> \
--keyword HELLOEUM \
--keyword-action AUTOMATIC_RESPONSE \
--keyword-message "Inbound works! Your message reached AWS and this reply came back automatically." \
--region <REGION>
```
If the auto-response doesn't come back, check these IN ORDER before anything else — this
step fails silently, and without a configuration set there is no inbound telemetry to inspect:
1. **Keyword match is exact.** The message must be ONLY the keyword — autocorrect adding
punctuation (`HELLOEUM.`), predictive text, or leading/trailing words all miss silently.
2. **It must arrive over RCS, not SMS.** An SMS from the device won't route to the agent's
keyword handler. Confirm the thread shows RCS before blaming configuration.
3. **Allow ~60s** on a freshly activated agent.
4. The console deep link below is the DETERMINISTIC path — it pre-fills the exact keyword over
the right channel, removing all three ambiguities. Prefer it for the first test.
Walk the user through the console inbound test:
1. Open the agent's **Testing** tab in the console (sign in first if needed):
`https://<REGION>.console.aws.amazon.com/sms-voice/home?region=<REGION>#/rcs-agents?agent-id=<AGENT_ID>&tab=testing`
2. Click **Inbound deep link**, enter `HELLOEUM` as the message body, generate the link.
3. Scan the QR code with the phone — the message is pre-filled. Send it.
4. The auto-response should arrive within seconds.
## Verify
All three must be true, confirmed by the user and by API:
1. `TestingAgent.Status` is `ACTIVE` and the verified number shows `VERIFIED`.
2. The user received the branded outbound message (step 8).
3. The user sent `HELLOEUM` and received the auto-response (step 9).
Print a summary table for the user: brand, `AGENT_ID`, region, console link.
## Next steps
- **Send rich messages** — cards, carousels, suggestion buttons, webviews: the send-apis guide.
When a message includes media (`SendRcsMessage` FileMessage or card media), the `FileUrl`
and `ThumbnailUrl` fields accept **either an `https://` URL or an `s3://` URI**
(pattern `^(https://|s3://).+$`) pointing to a supported media file — image, video, audio, or
PDF (see the [RCS file messages guide](https://docs.aws.amazon.com/sms-voice/latest/userguide/rcs-file-messages.html)
for the current supported media types and limits). If the media is already reachable at a URL, pass that URL directly — **do NOT create
an S3 bucket to re-host media you already have a URL for.** Use `s3://` only when the media
already lives in S3 (the service then downloads, rehosts, and presigns it; the bucket needs a
resource policy granting `sms-voice.amazonaws.com` read access, scoped with `aws:SourceAccount`
— and `aws:SourceArn` when a specific resource is known — to prevent cross-account
confused-deputy access, default encryption at rest enabled (SSE-S3 or SSE-KMS), and a
deny-non-TLS statement (`"Condition":{"Bool":{"aws:SecureTransport":"false"}}`) so media is only
retrieved over HTTPS). Do NOT guess or hand-construct
a media URL (e.g. inventing a CDN path) — use a URL the user provides or one you have verified
resolves to the file.
- **React to inbound messages with code** — wire an SNS topic (with SSE/KMS, `aws:SourceArn`/`aws:SourceAccount` condition keys, and authorized subscriptions only) and Lambda: the two-way-inbound-lambda guide
- **Rate-limit outbound sends**: Implement application-side throttling on message sends
(e.g., per-recipient and per-minute caps) to prevent abuse and stay within service quotas.
- **Go to production** — a test agent only reaches verified testers. To message real customers you
need a production agent launch registration: mechanics in [references/customer-go-to-production-guide.md](customer-go-to-production-guide.md),
per-country requirements (always discovered live) in the registration-requirements-by-country guide
- Raise the account's monthly spend limit before serious testing (check the current default
via `aws pinpoint-sms-voice-v2 describe-spend-limits --region <REGION>`):
request quota `L-2325465C`, then `set-text-message-spend-limit-override`.
## Security Considerations
- **Ephemeral credentials**: Prefer IAM Identity Center (SSO)/assumed-role over long-lived access keys.
- **Input validation**: Validate all user inputs (E.164, URL allowlist, length) before API calls.
- **Encryption**: Encrypt CloudTrail logs and CloudWatch Log groups with KMS CMK.
- **SNS topics**: Enable SSE with KMS, restrict with `aws:SourceArn`/`aws:SourceAccount`, authorize subscriptions.
- **Rate limiting**: Throttle outbound sends per-recipient and per-minute.
- **Deletion protection**: Enabled by default; confirm with user before cleanup.
## Failure modes
| Error / symptom | Cause | Fix |
|---|---|---|
| `AccessDeniedException` on any call | Missing IAM permissions or expired session | Re-auth (`aws sso login --profile <PROFILE>` or `aws configure`); identity needs the required scoped `sms-voice:` actions (not the `sms-voice:*` wildcard) |
| `ExpiredTokenException` | Session expired mid-task | Re-auth, retry the same command |
| `Invalid choice '<operation>'` (any `pinpoint-sms-voice-v2` op, at any step) | Local client's service model is stale or partial — a partial model can carry some ops (e.g. control-plane `describe-rcs-agents`) while omitting others (e.g. data-plane `send-rcs-message`) | Not permissions/account. First check for a stale local override: `ls ~/.aws/models/pinpoint-sms-voice-v2/` — rename it away if present (it shadows the SDK) and retry. If no override, the CLI is too old — give the user the commands to upgrade to the latest AWS CLI v2 (`aws --version`, then the install/update guide); do not run the upgrade yourself. Locked environment → boto3 fallback; if boto3 is also stale, apply the same override check. Resources already created remain valid — resume after fixing |
| Registration `REQUIRES_UPDATES` | A field was denied | `describe-registration-field-values --registration-id <REG_ID>` → find `DeniedReason` → fix the cause → `create-registration-version` → **re-set ALL fields (new versions start empty)** → resubmit |
| `ACCENT_COLOR_CONTRAST_INSUFFICIENT` | Color too light | Pick a darker hex from step 1's list; full re-populate as above |
| Unknown/missing field errors at submit | Field set drifts by CLI/service version | Reconcile against `describe-registration-field-definitions` output; set what it lists |
| No tester invitation after 20 min | Too fast after creation, or agent not ACTIVE | Confirm 120 s elapsed post-creation and `TestingAgent.Status: ACTIVE`; delete and re-create the verified number |
| `DESTINATION_COUNTRY_BLOCKED_BY_PROTECT_CONFIGURATION` | Country rule set blocks destination | Step 8 protect-configuration fix |
| `DESTINATION_PHONE_NUMBER_OPTED_OUT` | Number previously sent STOP | Step 8 opt-out fix, then resend |
| `MONTHLY_SPEND_LIMIT_REACHED` | Default spend limit exhausted (check current default with `describe-spend-limits`) | Quota increase `L-2325465C`, then `set-text-message-spend-limit-override` |
| Message arrives as SMS, not RCS | Agent not ACTIVE, device lacks RCS, or wrong origination identity | Verify agent status; confirm device RCS support; `--origination-identity` must be the `rcs-…` ID or its full ARN |
| Message doesn't arrive at all | Not a verified tester | Test agents ONLY deliver to `VERIFIED` destination numbers — check step 7 |
| `MessageId` returned but nothing arrives (number IS verified) | No delivery telemetry exists without a configuration set; cause is usually device-side | Wait up to 2 min → resend once → check device RCS state (Android: RCS chats "Connected"; iPhone: RCS Messaging on). Don't build logging infra mid-task; for telemetry see the configuration-sets-and-event-destinations guide |
## Cleanup (destructive — confirm with the user first)
```bash
# <VDN_ID> came from the create-verified-destination-number response (or describe-verified-destination-numbers)
aws pinpoint-sms-voice-v2 delete-verified-destination-number --verified-destination-number-id <VDN_ID> --region <REGION>
aws pinpoint-sms-voice-v2 delete-registration --registration-id <REG_ID> --region <REGION>
aws pinpoint-sms-voice-v2 delete-registration-attachment --registration-attachment-id <LOGO_ID> --region <REGION>
aws pinpoint-sms-voice-v2 delete-registration-attachment --registration-attachment-id <BANNER_ID> --region <REGION>
aws pinpoint-sms-voice-v2 update-rcs-agent --rcs-agent-id <AGENT_ID> --no-deletion-protection-enabled --region <REGION>
aws pinpoint-sms-voice-v2 delete-rcs-agent --rcs-agent-id <AGENT_ID> --region <REGION>
```
If `delete-rcs-agent` fails with `ConflictException RESOURCE_NOT_EMPTY` right after the
registration delete, the testing agent is still tearing down asynchronously — wait ~20-30s and
retry. Attachment deletes can hit the same lag (`RESOURCE_DELETION_NOT_ALLOWED`); same fix.
references/registration-core-engine.md
# AWS End User Messaging: The core registration engine
> Every registered origination identity — 10DLC brand/campaign, toll-free, short code, sender
> ID, RCS country launch, and Notify tier upgrade — runs through the SAME registration engine.
> This file is that engine: create a registration, derive its field set live, fill every field,
> submit, poll two independent state machines, and recover from a denial by re-filling ALL
> fields. WHICH type a given country and identity needs, and what that type requires, is a
> live-discovery question and is deliberately not here:
> [references/registration-requirements-by-country.md](registration-requirements-by-country.md).
> Production reviews range from about a day to several months by type and country — the job is
> to submit correctly, set expectations, and leave the user a monitoring loop.
You run every command yourself. The user provides inputs and confirms results. Stop and consult
the Failure modes table on any error.
## Session state
| Value | Format | Source |
|---|---|---|
| `REGION` | e.g. `us-east-1` | ask the user (default `us-east-1`) |
| `REG_TYPE` | e.g. `<ISO2>_RCS_LAUNCH_REGISTRATION` | discovery per registration-requirements-by-country |
| `REG_ID` | `registration-...` | CreateRegistration response |
| `RESOURCE_ID` | `rcs-...` / `phone-...` | the resource the registration gates, if any |
| `ATTACHMENT_ID` | `attachment-...` | CreateRegistrationAttachment response, if the schema has ATTACHMENT fields |
## Prerequisites
- AWS account with the AWS End User Messaging SMS and Voice service enabled.
- IAM role with least-privilege `sms-voice:` permissions for the registration actions used below
(`CreateRegistration`, `CreateRegistrationAssociation`, `DescribeRegistrationFieldDefinitions`,
`PutRegistrationFieldValue`, `SubmitRegistrationVersion`, `DescribeRegistrations`,
`CreateRegistrationVersion`, and `RequestPhoneNumber` where a number is acquired). Scope the
policy to these actions; do not grant `sms-voice:*` or `*FullAccess`. Prefer assuming an IAM
role with ephemeral credentials over long-lived access keys.
- Validate all input parameters (region, ISO country code, field values, attachment paths)
before calling APIs.
- These operations are available through the AWS MCP Server, which provides sandboxed execution,
audit logging, and observability. When the MCP server is not available, use the AWS CLI
commands shown below.
- Confirm the AWS CLI is available (`aws --version`). On CLI v1, URL-valued TEXT fields fail via
a legacy paramfile behavior — use the `--cli-input-json` form shown in step 5, which works on
both versions.
## The engine — one lifecycle for every registration type
### 1. Pick the type
Which registration type the target country and identity type need is discovered live, never
memorized — commands and doc pointers in
[references/registration-requirements-by-country.md](registration-requirements-by-country.md).
Save `REG_TYPE` and the type's `AssociationBehavior`.
### 2. Create, and associate if the resource already exists
```bash
aws pinpoint-sms-voice-v2 create-registration \
--registration-type <REG_TYPE> --region <REGION>
aws pinpoint-sms-voice-v2 create-registration-association \
--registration-id <REG_ID> --resource-id <RESOURCE_ID> --region <REGION>
```
Save `RegistrationId` as `REG_ID`. The second call applies only when the type gates an existing
resource (for example, an RCS agent). `AssociationBehavior` on the type definition says when the
association must happen:
- `ASSOCIATE_BEFORE_SUBMIT` — **you MUST link the resource before submitting, or the submission
is invalid** (RCS agents, including RCS country launches).
- `ASSOCIATE_ON_APPROVAL` — the resource is auto-provisioned on approval (sender IDs — no
separate request step).
- `ASSOCIATE_AFTER_COMPLETE` — buy or associate the resource only after COMPLETE (phone numbers).
### 3. Derive the field set — the schema is always live
```bash
aws pinpoint-sms-voice-v2 describe-registration-field-definitions \
--registration-type <REG_TYPE> --region <REGION>
```
Save per field: `FieldPath`, `FieldType`, `FieldRequirement`, and validation constraints. Field
sets vary by type and drift over time — derive, do not memorize. Do not filter this output;
conditional field requirements depend on the complete definition shape.
### 4. Upload attachments first
Only if the schema has ATTACHMENT fields: one `create-registration-attachment` per file
(`--attachment-body` and `--attachment-url` are mutually exclusive), within the size and format
constraints the field definition reports for that attachment field. Poll to `UPLOAD_COMPLETE`
before any field references the attachment ID. Prefer `--attachment-body` with a local
`fileb://` path over a public URL for documents that contain business-identifying information.
### 5. Fill every field — one call per field, parameter set by the field's type
**Where the values come from depends on the registration type.** For a **TEST** registration
(e.g. `TEST_RCS_LAUNCH_REGISTRATION`, which only reaches verified testers), default to synthetic
placeholder values — do not interview the user field-by-field or run live field discovery to
decide what to ask; go straight to synthetic defaults, then **show the user the full set of
placeholder values and let them opt out** ("these are synthetic; tell me if you'd rather provide
your own for any field") and confirm before submitting. For a **PRODUCTION** registration (10DLC
brand/campaign, toll-free, short code, sender ID, RCS country launch), do NOT use synthetic
values: these are submitted to carrier/partner review and synthetic `.example.com` contacts or an
invented description will be denied. Collect **real but non-sensitive business contacts** from the
user (as the Security Considerations require), show them the full set, and confirm before submit.
In both cases never submit invented data unconfirmed. Treat any real values the user provides as
sensitive business-identifying PII (contact details, legal names, addresses): use them only to
populate the registration and to confirm back — do not echo them into responses or logs beyond
what confirmation requires. Because tooling may log API payloads (CloudTrail data events, MCP
server or agent-framework audit logs), the `put-registration-field-value` values can land in those
logs too — ensure any such log destinations are encrypted with a KMS key so the PII is protected at
the infrastructure layer, not just in the agent's own output: CloudWatch Logs log groups with a KMS
CMK, and the CloudTrail trail's S3 destination bucket with SSE-KMS (a dedicated CMK if the trail
records these data events).
```bash
aws pinpoint-sms-voice-v2 put-registration-field-value --registration-id <REG_ID> \
--field-path "<FIELD_PATH>" --text-value "<value>" --region <REGION>
```
| FieldType | Parameter |
|---|---|
| TEXT | `--text-value "<value>"` |
| SELECT | `--select-choices "<choice>"` |
| ATTACHMENT | `--registration-attachment-id "<id>"` |
`--field-values` does not exist; do not invent it. SELECT options live under
`SelectValidation.Options` as a list of plain strings. On AWS CLI v1, URL-valued TEXT fields
fail because a legacy paramfile feature fetches `https://` values as remote files — use the
`--cli-input-json` form, which works on both versions:
```bash
aws pinpoint-sms-voice-v2 put-registration-field-value --region <REGION> --cli-input-json \
'{"RegistrationId":"<REG_ID>","FieldPath":"<FIELD_PATH>","TextValue":"https://www.example.com"}'
```
Where a field takes a phone number, provide it in E.164 format (e.g., `+12065550123`).
### 6. Audit, submit, and poll BOTH state machines
Audit what is SET against what is REQUIRED before submitting:
```bash
aws pinpoint-sms-voice-v2 describe-registration-field-values \
--registration-id <REG_ID> --region <REGION> \
--query 'RegistrationFieldValues[].FieldPath' --output text | tr '\t' '\n' | sort > /tmp/set.txt
aws pinpoint-sms-voice-v2 describe-registration-field-definitions \
--registration-type <REG_TYPE> --region <REGION> \
--query "RegistrationFieldDefinitions[].FieldPath" --output text | tr '\t' '\n' | sort > /tmp/req.txt
comm -13 /tmp/set.txt /tmp/req.txt
```
Do not hardcode a filter on the requirement value — derive the field set from the full
definition and let the live schema decide applicability (the same reason you do not filter the
definition output above). Any line printed is a defined field you have not set; confirm from the
field's own definition whether it applies to your case before submitting. Then submit and
**poll BOTH state machines — they move independently, and you MUST check both**: registration
status via `describe-registrations` AND version status via `describe-registration-versions`:
```bash
aws pinpoint-sms-voice-v2 submit-registration-version --registration-id <REG_ID> --region <REGION>
aws pinpoint-sms-voice-v2 describe-registrations --registration-ids <REG_ID> \
--query 'Registrations[0].RegistrationStatus' --region <REGION>
aws pinpoint-sms-voice-v2 describe-registration-versions --registration-id <REG_ID> --region <REGION>
```
Registration status: `CREATED → SUBMITTED → REVIEWING → COMPLETE`, with branches
`REQUIRES_UPDATES` and `AUTHENTICATION_REQUIRED` (a human step, typically email 2FA). Version
status: `DRAFT → SUBMITTED → REVIEWING → APPROVED | DENIED`. The rules that never vary:
- **Only COMPLETE counts.** While SUBMITTED or REVIEWING, the identity behaves as unregistered —
messages may go out with a generic "NOTICE"/"Unverified" sender or not at all, by country.
Never tell the user "submitted, so we're good."
- **Locked while REVIEWING.** No field edits, no disassociation — wait for the verdict or discard
the version.
- **Denied means a FULL RESET — this is the #1 cause of a second denial.** On
`REQUIRES_UPDATES`/`DENIED`, read every per-field `DeniedReason` (some types include generated
fix suggestions — read them), then call `create-registration-version`. **The new version
starts EMPTY: every field value is reset, not just the denied ones.** You MUST re-run
`put-registration-field-value` for EVERY field (attachments re-reference by existing ID), then
re-audit set-vs-required and resubmit. Updating only the denied field on the existing version
does not work.
- **Billing continues regardless.** Purchased numbers bill monthly whatever the registration
status; releasing the number is the only off switch.
### 7. Acquire the resource (for `ASSOCIATE_AFTER_COMPLETE` types)
Once COMPLETE, request the number against the registration:
```bash
aws pinpoint-sms-voice-v2 request-phone-number \
--iso-country-code <ISO2> --message-type TRANSACTIONAL \
--number-capabilities SMS --number-type <TYPE> \
--registration-id <REG_ID> --region <REGION>
```
Save `PhoneNumberId`; lifecycle `PENDING → ACTIVE`. Capabilities (SMS/MMS/VOICE) are immutable
after purchase — choose them now. Throughput (MPS) does not auto-raise on approval; increases
are a separate Support request. Which number types a country offers, prerequisite
registrations, and throughput and ETAs are live-discovery questions:
[references/registration-requirements-by-country.md](registration-requirements-by-country.md).
## RCS country launch — the same engine, one wrinkle
A production RCS launch is one more registration type run through the engine above. The test
agent from the RCS quickstart stays; production adds a per-country launch registration to the
SAME agent (`ASSOCIATE_BEFORE_SUBMIT`: associate the agent before submitting). Discover the
launch type for the target country (names follow `<ISO2>_RCS_LAUNCH_REGISTRATION`;
`TEST_RCS_LAUNCH_REGISTRATION` exists for exercising the launch flow against a test device),
create it, associate
the agent, then run steps 3–6. The launch field set is larger than the test registration
(launch details, carrier-tester access instructions, messaging samples, opt-in evidence) and a
compliance video demonstrating the agent's HELP/STOP/opt-in flows is typically required — record
the real agent behavior. Carrier review runs for months; set that expectation up front.
Per-carrier launch monitoring, SMS fallback pools, and production hygiene (keywords,
configuration sets, protect configurations) are covered in
[references/customer-go-to-production-guide.md](customer-go-to-production-guide.md).
## Verify
**Demonstrable outcome.** The engine's artifacts are state transitions — capture them as
before/after describe output the user can see: `RegistrationStatus` reaching `COMPLETE`, the
identity flipping to `ACTIVE`. Confirm each with a describe call, not from memory. Reviews take
days to months, so most sessions end mid-transition. When yours does, say so explicitly and
leave the user the exact poll:
```bash
aws pinpoint-sms-voice-v2 describe-registrations --registration-ids <REG_ID> \
--region <REGION> --query 'Registrations[0].RegistrationStatus'
```
The day that returns `COMPLETE`, the transition in that output — next to the earlier `REVIEWING`
the user recorded — is the demonstrable finish.
## Next steps
- Which registration type a country needs, and what it requires — always live:
[references/registration-requirements-by-country.md](registration-requirements-by-country.md)
- RCS test agent quickstart (verified testers only): [references/hello-rcs-test-agent.md](hello-rcs-test-agent.md)
- Notify OTP quickstart: [references/hello-notify-otp.md](hello-notify-otp.md)
- Production launch procedures — sandbox exit, RCS carrier monitoring and fallback pools, Notify
tier upgrade, production hygiene: [references/customer-go-to-production-guide.md](customer-go-to-production-guide.md)
## Security Considerations
- **Least-privilege IAM.** Scope policies to the specific `sms-voice:` registration actions;
never use `*FullAccess` or `sms-voice:*`.
- **Ephemeral credentials.** Assume an IAM role with ephemeral credentials; never embed
long-lived access keys in code, config, or environment variables.
- **Sensitive registration content.** Registration contact fields (email, phone, URLs) and
attachments are submitted to partner review; use real but non-sensitive business contacts, and
prefer local `fileb://` attachment bodies over public URLs. Enable CloudTrail logging for
`sms-voice` API calls and encrypt CloudTrail logs and CloudWatch Log groups with a KMS CMK.
- **Monitoring.** Registrations can sit in `REVIEWING` for weeks or fail silently to
`REQUIRES_UPDATES`/`DENIED`. Create a CloudWatch alarm or EventBridge rule on registration
status changes so the user is notified proactively rather than relying solely on manual polling.
Status-change notifications can carry sensitive business information (e.g. per-field denial
reasons), so encrypt the notification target: use an SNS topic with KMS encryption
(`alias/aws/sns` or a customer-managed key), add a resource policy with `aws:SourceAccount` and
`aws:SourceArn` condition keys to prevent confused-deputy access, restrict `sns:Subscribe` to
known authorized accounts, and subscribe HTTPS-only endpoints so only authorized personnel
receive registration status details.
- **Input validation.** Validate all user inputs (E.164 for phones, URL scheme allowlist, length
checks) before passing them to APIs.
- **AWS security references.** These recommendations follow AWS security best practices — see
[IAM Best Practices](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html) for
least-privilege and ephemeral credentials, and
[AWS KMS Best Practices](https://docs.aws.amazon.com/kms/latest/developerguide/best-practices.html)
for encrypting CloudTrail logs and CloudWatch Log groups.
## Failure modes
| Error / symptom | Cause | Fix |
|---|---|---|
| Registration `REQUIRES_UPDATES` | Field(s) denied | `describe-registration-field-values` → per-field `DeniedReason` → `create-registration-version` → re-populate ALL fields → resubmit. Rejection feedback may include generated guidance — read it |
| Registration stuck in REVIEWING | Reviews take days to months by type | Poll daily, not per-minute; give the user the current ETA from the live sources in registration-requirements-by-country |
| Sends show "NOTICE"/"Unverified" sender | Registration not yet COMPLETE | Wait for COMPLETE; only COMPLETE counts |
| `ConflictException` editing a registration | Locked while REVIEWING | Wait for a terminal state, or discard the version |
| Number request fails with a registration error | `ASSOCIATE_AFTER_COMPLETE` type requires COMPLETE before purchase | Finish the registration first, then `request-phone-number` |
| Throughput stuck at default MPS after approval | MPS increases are not automatic | Separate Support request for MPS |
| Registration type name from a doc or memory rejected as invalid | Type names drift | Discover live via `describe-registration-type-definitions` — see registration-requirements-by-country |
## Cleanup (destructive — confirm with the user first)
Registrations and any purchased identities persist until removed; purchased numbers bill monthly
while owned. To stop: `delete-registration`, and `release-phone-number` / `release-sender-id`
for acquired identities — all irreversible. Confirm with the user, and note released numbers may
not be recoverable.
```bash
aws pinpoint-sms-voice-v2 delete-registration --registration-id <REG_ID> --region <REGION>
```
references/registration-requirements-by-country.md
# AWS End User Messaging: Registration requirements by country — live discovery
> Country- and number-type-specific registration requirements change over time: registration
> types appear, field sets grow, review ETAs shift, and countries move between
> optional-registration and required-registration. Do not rely on this file's — or any cached
> copy's — per-country specifics. Inspect the live sources at the start of every run. This file
> is deliberately mostly pointers: the discovery commands that stay current, and the official
> docs pages that hold the human-readable per-country rules. The universal mechanics of working
> a registration (state machines, field filling, denial recovery) live in
> [references/registration-core-engine.md](registration-core-engine.md) — this file
> answers only "which type, and what does it require."
You run every command yourself. **Your FIRST action MUST be to run
`describe-registration-type-definitions` for the region — before you write any part of the
answer. You MUST NOT state which registration a country needs, or name any registration type,
until that command's output has returned this session; then you MUST run
`describe-registration-field-definitions` on the matched type for the authoritative field set.**
A memorized or example-derived answer — including anything from this file's examples or your own
training knowledge — is a defect, no matter how plausible. Before stating any conclusion you MUST
also state the reconciliation rule (where docs and live field definitions disagree, the live
definitions win). Treat the API output as the source of truth. Stop and
consult the Failure modes table on any error.
## Session state
| Value | Format | Source |
|---|---|---|
| `REGION` | e.g. `us-east-1` | ask the user (default `us-east-1`) |
| `ISO2` | ISO 3166-1 alpha-2, e.g. `XX` | the user's target country |
| `REG_TYPE` | e.g. `<ISO2>_RCS_LAUNCH_REGISTRATION` | matched from discovery below |
## Prerequisites
- AWS account with the AWS End User Messaging SMS and Voice service enabled.
- IAM role with least-privilege permissions for the read-only `sms-voice:` describe actions used
below (`DescribeRegistrationTypeDefinitions`, `DescribeRegistrationFieldDefinitions`,
`DescribeRegistrationSectionDefinitions`). Prefer assuming an IAM role with ephemeral
credentials over long-lived access keys.
- Validate all input parameters (region, ISO country code, registration type) before calling APIs.
- These operations are available through the AWS MCP Server, which provides sandboxed execution,
audit logging, and observability. When the MCP server is not available, use the AWS CLI
commands shown below.
- Confirm the AWS CLI is available (`aws --version`). On CLI v1, URL-valued fields and
`aws logs tail` behave differently — the core-engine file flags v1 forms where they matter.
## Live discovery — the API is the authority
Three read-only calls give the current truth for the account and region.
First, list every registration type available in the region:
```bash
aws pinpoint-sms-voice-v2 describe-registration-type-definitions --region <REGION>
```
Match the returned names against the user's goal:
| Goal | Type-name patterns to look for |
|---|---|
| RCS launch in a country | `<ISO2>_RCS_LAUNCH_REGISTRATION`; also `TEST_RCS_LAUNCH_REGISTRATION` for the testing flow |
| Sender ID in a country | `<ISO2>_SENDER_ID_REGISTRATION` |
| Short code in a country | `<ISO2>_SHORT_CODE_REGISTRATION` |
| Long code in a country | `<ISO2>_LONG_CODE_REGISTRATION` |
| US 10DLC | `US_TEN_DLC_*` (brand registration, brand vetting, campaign registration) |
| US toll-free | `US_TOLL_FREE_REGISTRATION` |
| Notify tier / brand | `NOTIFY_*` (filter the live output for types containing `NOTIFY` — e.g. the tier-upgrade and brand-verification variants) |
Save the matched type name as `REG_TYPE`, plus its `SupportedAssociations` and
`AssociationBehavior`. If no type matches the country/identity combination, that combination is
not registrable via the API — check the docs pages below for whether it needs a Support case or
is unsupported.
Then derive the authoritative field set for that type, and optionally its section grouping:
```bash
aws pinpoint-sms-voice-v2 describe-registration-field-definitions \
--registration-type <REG_TYPE> --region <REGION>
aws pinpoint-sms-voice-v2 describe-registration-section-definitions \
--registration-type <REG_TYPE> --region <REGION>
```
The field definitions output is the only authoritative statement of what a registration
requires: field paths, types (TEXT/SELECT/ATTACHMENT), REQUIRED vs OPTIONAL, and validation
constraints. It is paginated — walk all pages. Do not filter this output; conditional field
requirements depend on context from the complete definition shape.
## Official docs pages — the human-readable per-country rules
Fetch the relevant page(s) each run over HTTPS (append `.md` to any slug for a markdown variant):
| Page | URL | What it holds |
|---|---|---|
| Registration overview | <https://docs.aws.amazon.com/sms-voice/latest/userguide/registrations.html> | Which registrations exist per country/identity type; console forms vs Support cases; status-vs-sending-behavior rules |
| Review ETAs | <https://docs.aws.amazon.com/sms-voice/latest/userguide/registration-eta.html> | Estimated review time per country + identity type |
| Sender ID rules | <https://docs.aws.amazon.com/sms-voice/latest/userguide/sender-id.html> | Sender ID character rules, registered-vs-dynamic per country, the casing rule |
| Country capabilities hub | <https://docs.aws.amazon.com/sms-voice/latest/userguide/phone-numbers-sms-support-by-country.html> | SMS/MMS capabilities and restrictions; links to per-country supported-countries tables |
| RCS country launches | <https://docs.aws.amazon.com/sms-voice/latest/userguide/rcs-country-launch.html> | Per-country RCS launch process: use-case categories, carrier states, timelines |
## The reconciliation rule
1. Run the discovery commands for the target country and identity type.
2. Fetch the matching docs page(s) for the human context: prerequisites, evidence to prepare
(opt-in proof, compliance video, letters of authorization), ETAs, and country specifics.
3. Reconcile. Where the docs and the live field definitions disagree — a field the docs do not
mention, a type the docs still call by an old name — the live field definitions win. They are
the tiebreaker; docs lag the service.
4. Execute the mechanics from [references/registration-core-engine.md](registration-core-engine.md)
with the discovered `REG_TYPE` and field set.
## Worked example: "the user wants to send SMS to a country `<ISO2>`"
This example uses the placeholder `<ISO2>` for the user's target country. It never names a real
country or a real registration type — substitute what your own discovery returns this run.
Discover what the region offers for `<ISO2>`:
```bash
aws pinpoint-sms-voice-v2 describe-registration-type-definitions --region <REGION>
```
Inspect the returned `RegistrationType` names for entries starting with `<ISO2>_`. Then read, each
run: the country capabilities hub (follow its supported-countries link to the `<ISO2>` row for
which origination identity types that country supports, whether registration is required, two-way
support); the registration overview page (is there an `<ISO2>` form, or a Support-case country);
and the ETA page (review time for the identity type you land on).
Conclude from what the pages and `describe-registration-type-definitions` returned this run —
for example, whether `<ISO2>` supports sender IDs without registration or a registered long code
via an `<ISO2>_LONG_CODE_REGISTRATION` type with an N-day review. State it from this run's output,
never from this example, and never from prior knowledge of any specific country.
**State the reconciliation rule in your answer.** Treat `describe-registration-field-definitions`
as the authoritative field set, and say so explicitly: *where the documentation pages and the
live field definitions disagree, the live definitions win, because the docs lag the service.*
Name `describe-registration-field-definitions` as the source of the field list. If the live
output disagrees with the pages, follow this rule and note the discrepancy to the user. Then
run the machinery: [references/registration-core-engine.md](registration-core-engine.md).
## Verify
**Demonstrable outcome.** This file's artifact is the live discovery itself, on screen this run:
the `describe-registration-type-definitions` output naming the matched `REG_TYPE`, the
`describe-registration-field-definitions` field list for it (all pages), and the
one-paragraph conclusion derived for the user's country from this run's output — "country XX
needs `<REG_TYPE>`; N required fields including `<example fields>`; review ETA per the docs
page." If the live output and the docs pages disagreed, the note of that discrepancy is part of
the deliverable. Nothing here is demonstrable from memory — if the discovery commands were not
run this session, the job is not done; leave the user the three read-only calls above as the
exact sequence that produces the answer.
## Next steps
- The mechanics — state machines, field filling, denial recovery:
[references/registration-core-engine.md](registration-core-engine.md)
- Quickstart RCS test agent (verified testers only): [references/hello-rcs-test-agent.md](hello-rcs-test-agent.md)
- Quickstart Notify OTP: [references/hello-notify-otp.md](hello-notify-otp.md)
## Security Considerations
- **Least-privilege IAM.** This flow is read-only discovery — scope the policy to the specific
`sms-voice:` describe actions used above (`DescribeRegistrationTypeDefinitions`,
`DescribeRegistrationFieldDefinitions`, `DescribeRegistrationSectionDefinitions`); never use
`*FullAccess` or `sms-voice:*`.
- **Ephemeral credentials.** Assume an IAM role with ephemeral credentials; never embed
long-lived access keys in code, config, or environment variables.
- **Input validation.** Validate all inputs (region, ISO country code, registration type)
before passing them to APIs, and fetch the docs pages over HTTPS.
- **Logging.** Enable CloudTrail logging for `sms-voice` API calls in the account and encrypt
CloudTrail logs and CloudWatch Log groups with a KMS CMK to maintain an audit trail of discovery
queries — who queried registration type definitions and when — for compliance and incident
response.
- **AWS security references.** These recommendations follow AWS security best practices — see
[IAM Best Practices](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html) for
least-privilege and ephemeral credentials, and
[AWS KMS Best Practices](https://docs.aws.amazon.com/kms/latest/developerguide/best-practices.html)
for encrypting CloudTrail logs and CloudWatch Log groups.
## Failure modes
| Error / symptom | Cause | Fix |
|---|---|---|
| A remembered or documented registration type name is rejected as invalid | Type names drift (e.g. a stale `MANAGED_ROUTES_BRAND_VERIFICATION`) | `describe-registration-type-definitions` — use only names it returns |
| Submit fails on a field no doc mentioned | Field sets grow over time; docs lag | Re-derive with `describe-registration-field-definitions`; the live schema is the tiebreaker |
| A docs URL above returns 404 | Docs pages get reorganized | Search the user guide root (`what-is-service.html`) for the topic's current slug; do not skip the fetch |
| No `<ISO2>_*` type exists for the target country | Combination not API-registrable | Check the registration overview page — Support-case-only country, or unsupported |
SKILL.md
---
name: aws-sms-voice
description: >
Onboards RCS Business Messaging and Notify OTP via the pinpoint-sms-voice-v2
AWS CLI. Covers creating a branded RCS test agent (logo, banner, rich cards,
suggested replies), adding verified testers, and sending/receiving the first RCS
message; and configuring AWS End User Messaging Notify to send one-time-passcode
(OTP) verification codes through AWS-managed phone numbers without buying a
number or completing carrier registration. Also discovers which registration type a
country requires from live API definitions and runs the universal registration
lifecycle (create, derive fields, fill, submit, recover from denial, resubmit) shared
by 10DLC, toll-free, short code, sender ID, RCS country launch, and Notify tier
upgrade. Applicable to RCS agent setup, RCS message send/receive, Notify OTP
configuration, verification-code delivery, registration requirements by country, and
registration submission. Does not cover WhatsApp (see aws-social-messaging).
version: 1
---
# AWS End User Messaging — SMS, Voice, RCS & Notify
## Overview
AWS End User Messaging SMS and Voice (the `pinpoint-sms-voice-v2` API) is the
control and data plane for SMS, Voice, RCS Business Messaging, and Notify (OTP).
This skill covers the two fastest onboarding paths — each reaches
a real message on a real phone in about five minutes, with no phone-number
purchase and no carrier registration:
1. **Create an RCS test agent and send your first branded message** — a branded
RCS Business Messaging agent, a verified test device, and a rich outbound +
inbound message. See [references/hello-rcs-test-agent.md](references/hello-rcs-test-agent.md).
2. **Send an OTP with Notify** — a one-time-passcode text through
AWS-managed sending identities and pre-approved templates. See
[references/hello-notify-otp.md](references/hello-notify-otp.md).
Beyond onboarding, this skill takes an identity to production registration in three steps:
1. **Find which registration a country requires** — discover the registration type and
its live field set for a target country and identity type, reconciled with the
official docs. See [references/registration-requirements-by-country.md](references/registration-requirements-by-country.md).
2. **Run the registration lifecycle** — the universal engine (create, derive fields,
fill, submit, poll, recover from denial, resubmit) shared by every registration type,
including RCS country launch. See [references/registration-core-engine.md](references/registration-core-engine.md).
3. **Take an identity to production** — exit the SMS sandbox, launch RCS in a country with
carrier monitoring and SMS fallback, upgrade Notify to Advanced, and apply production
hygiene (keywords, configuration sets, protect configurations). See
[references/customer-go-to-production-guide.md](references/customer-go-to-production-guide.md).
## Guardrail — where this skill's files live (MCP vs local install)
This skill can be loaded two ways, and the `references/` files resolve differently:
- **Loaded through the AWS MCP `retrieve_skill` tool.** Reference files do not
exist on the local filesystem. Fetch them via `retrieve_skill` with the `file`
parameter (e.g., `file="references/hello-rcs-test-agent.md"`). Do NOT
`file_read` these paths locally — they are not there.
- **Installed locally** (e.g., `.kiro/skills/aws-sms-voice/`). Read references
from the local skill directory using the relative paths shown in this file.
This applies only to the skill's own packaged files. User-created artifacts
(brand-assets/, temp files) are always in the user's working directory.
Execute commands using available tools from the AWS MCP server when connected —
it provides sandboxed execution, audit logging, and observability. When the MCP
server is not available, fall back to the AWS CLI or shell as needed.
## Choosing a path
| You want to… | Go to |
|---|---|
| Send rich, branded messages (cards, logo, two-way) to your own test phone | [hello-rcs-test-agent.md](references/hello-rcs-test-agent.md) |
| Send a verification code / OTP to any supported phone, fastest possible path | [hello-notify-otp.md](references/hello-notify-otp.md) |
| Find out which registration a country needs and what fields it requires | [registration-requirements-by-country.md](references/registration-requirements-by-country.md) |
| Create and submit a registration of any type (10DLC, toll-free, sender ID, RCS launch, Notify tier) | [registration-core-engine.md](references/registration-core-engine.md) |
| Take an identity to production (sandbox exit, RCS carrier launch + fallback, Notify Advanced, hygiene) | [customer-go-to-production-guide.md](references/customer-go-to-production-guide.md) |
RCS test agents deliver **only to verified test devices** and require a short
brand registration (auto-reviewed in minutes). Notify sends to any recipient in
its supported countries immediately, but is limited to AWS-managed OTP templates.
Pick RCS to explore rich messaging; pick Notify for the shortest path to a
production-grade verification text.
## Before you start (both paths)
The **AWS MCP server is recommended** for the best experience here — it runs the many
`pinpoint-sms-voice-v2` and `sts` calls with sandboxed execution, audit logging, and
observability. It is not a hard requirement: every step also works with the plain AWS CLI or the
AWS Agent Toolkit when the MCP server is not available.
1. **Credentials.** Run `aws sts get-caller-identity --region <REGION>` (pass `--region` to use
the regional STS endpoint; the global `sts.amazonaws.com` endpoint is legacy). If it fails,
configure the user's auth (named profile / SSO / access keys — `--profile <PROFILE>` on every
command if a profile is in play) and prefer an assumed IAM role with ephemeral credentials. Each
reference file's Prerequisites carry the full auth, CloudTrail-audit, and CLI-freshness steps.
2. **Least-privilege access.** These tasks need a small set of specific
`sms-voice:` actions in the target account (the reference files enumerate the
read/write actions each step uses — for example `sms-voice:DescribeRcsAgents`,
`CreateRcsAgent`, `CreateRegistration`, `CreateNotifyConfiguration`, and
`SendNotifyTextMessage`). Scope the policy to exactly the actions the chosen
path uses; do not grant `sms-voice:*` or `*FullAccess`.
3. **Don't trust the CLI version.** Treat any `Invalid choice '<operation>'` on a
`pinpoint-sms-voice-v2` command, at any step, as a stale/partial local service model (not a
permissions or account problem). The RCS and Notify reference files (prerequisite 3) carry the
full reactive resolution: clear a stale `~/.aws/models/pinpoint-sms-voice-v2/` override, else
upgrade the AWS CLI, with a boto3 fallback. `AccessDeniedException` means the identity is
missing the required `sms-voice:` permissions.
Each reference file is self-contained: it tracks the IDs you collect as session
state, audits required inputs against the service before any submit, and ends
with a **Failure modes** table and a **Cleanup** section. Follow the file for the
path the user chose rather than improvising the API sequence — several steps fail
silently or depend on service-version-specific field sets that must be read live.
## Security Considerations
- **Least-privilege IAM.** Scope policies to specific `sms-voice:` actions; never
use `*FullAccess` or `sms-voice:*` in production. The reference files enumerate
the minimal read/write actions each onboarding path needs.
- **Ephemeral credentials.** Assume an IAM role with ephemeral credentials;
never embed long-lived access keys in code, config, or environment variables.
- **No secrets in messages or fields.** OTP codes and message bodies can surface
in CloudTrail and downstream logs — treat them as sensitive and never hardcode
real codes. Enable CloudTrail logging for `sms-voice` API calls and encrypt
CloudTrail logs and CloudWatch Log groups with a KMS CMK. Attach a
configuration set with an event destination (CloudWatch Logs or Kinesis) for
delivery monitoring before production traffic. Create CloudWatch alarms on
key security signals (repeated `AccessDeniedException`, unusual send volumes,
spend limit approaching threshold) for proactive alerting. Registration contact fields (email, phone, URLs) are submitted to
partner review; use real but non-sensitive business contacts.
- **Consent and opt-out.** Only message recipients who have opted in. Honor STOP /
opt-out state; the RCS path shows how to check the opt-out list before sending.
Test agents intentionally restrict delivery to verified test devices — do not
work around that to reach unconsented numbers.
- **Spend limits and abuse.** Default account spend limits are intentionally low;
raise them deliberately and monitor. Notify enforces a non-adjustable per-day
cap per destination — do not attempt to bypass it.
- **Destructive cleanup.** The teardown steps delete agents, registrations, and
configurations. They are irreversible — confirm with the user before running
cleanup, and note RCS agents have deletion protection enabled by default in the
onboarding flow.
## Additional Resources
- [AWS End User Messaging SMS User Guide](https://docs.aws.amazon.com/sms-voice/latest/userguide/what-is-service.html)
- [AWS CLI pinpoint-sms-voice-v2 Reference](https://docs.aws.amazon.com/cli/latest/reference/pinpoint-sms-voice-v2/)
- [RCS messaging on AWS End User Messaging](https://docs.aws.amazon.com/sms-voice/latest/userguide/rcs.html)
- [AWS End User Messaging Notify](https://docs.aws.amazon.com/sms-voice/latest/userguide/notify.html)
- [IAM Security Best Practices](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html)