Generated banner for Amazon Ses
Generated logo for Amazon Ses

Amazon Ses

AWSOfficialPublished by AWS

  • Skill
  • Workflow
  • Cloud and DevOps
  • Apache-2.0
  • No issues found

From the author

Guides Amazon SES onboarding for domain-based email sending. Covers identity configuration, production access, an optional first test send, and troubleshooting setup, authentication, or sending failures. Use when setting up SES, resuming incomplete onboarding, or leaving the sandbox. Does not cover inbound email or Mail Manager, SMS/voice, WhatsApp, SNS, Pinpoint, or WorkMail.
— the description by AWS

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

This skill provides a structured workflow for setting up Amazon SES for domain-based email sending. It assists users in configuring email identities, managing DKIM and custom MAIL FROM settings, and navigating the process of requesting production access from the SES sandbox. The skill ensures that all necessary AWS CLI operations are performed using the v2 API, validates inputs according to strict security rules, and helps troubleshoot common setup or authentication failures.

What it does

  • Configures domain identities for email sending in Amazon SES.
  • Manages DKIM and custom MAIL FROM domain authentication.
  • Guides users through the process of requesting production access.
  • Facilitates optional test email sends to verify configuration.
  • Troubleshoots common setup, authentication, and sending errors.
  • Validates all user-supplied inputs to ensure secure CLI execution.

When to use it

  • When setting up a new domain for email sending in Amazon SES.
  • When resuming an incomplete SES onboarding process.
  • When transitioning an account out of the SES sandbox.
  • When diagnosing issues like MessageRejected errors or failed identity verification.

Advantages

  • Automates complex multi-step AWS CLI configurations.
  • Ensures adherence to security best practices through input validation.
  • Provides clear guidance on state-based configuration to avoid redundant operations.
  • Maintains strict adherence to AWS v2 API requirements.

Usage examples

“Help me set up my domain example.com for Amazon SES.”
The agent will read the current account state, identify missing identity or DNS configurations, and guide you through the necessary steps to verify the domain.
“I am getting a MessageRejected error when trying to send email.”
The agent will check your current SES identity status and sandbox constraints to determine if the issue is due to an unverified sender or a restricted recipient.

Install

  1. Download the skill as a ZIP:

    https://mp-staging.autogineer.com/agent-skills/aws/amazon-ses/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip amazon-ses.zip -d .agents/skills

Documentation by AWS

Amazon SES

Overview

Recommended: Use the AWS MCP Server with SES permissions for sandboxed execution and CloudTrail audit logging. Without MCP: All operations use standard AWS CLI syntax (aws sesv2 ...).

Takes a developer who is not an email authentication expert from an empty AWS account to a real, authenticated email in their inbox, and on to production sending if AWS approves the request.

Read the account and identity state before touching anything, and complete only the parts of domain setup that are actually missing. Request production access only after domain setup is complete and the user has given explicit consent. A test send is optional: run it when the user asks for one, to a recipient the account's current state permits.

Routing

If the user wants to...Read
Get started with SES, set up email sending, send a first or test email, move out of the sandbox, or diagnose MessageRejected: Email address is not verifiedSES onboarding: zero to first delivered email
Set up a domain for sending, configure email authentication, or troubleshoot DKIMSetting up SES domain identity

Guardrail — where this skill's own files live (MCP vs local install)

This skill can be loaded two ways, and they resolve its own bundled files from different places. Determine how the skill was loaded before reading a reference:

  • Loaded through the AWS MCP retrieve_skill tool: the skill is not on the local filesystem. You MUST fetch each reference via retrieve_skill with the file parameter (e.g. file="references/onboarding.md"). Do NOT file_read these paths locally — they do not exist on disk.

  • Installed locally (e.g. ~/.kiro/skills/amazon-ses/, .kiro/skills/amazon-ses/, or ~/.claude/skills/amazon-ses/): read files from the local skill directory using the relative paths above.

This distinction applies only to the skill's own packaged files. User data and session artifacts are always read from and written to the user's working directory — never fetch or write customer data through retrieve_skill.

Critical Rules

  • MUST use the SES v2 API: aws sesv2 ..., never the v1 aws ses ... commands. A v1 command reports no error signalling the wrong API was chosen, so the mistake is silent, and the operations this journey needs — production-access requests, and DKIM plus MAIL FROM attributes in a single identity read — exist only in v2. Models trained on published CLI examples tend to default to the v1 syntax.

  • MUST NOT volunteer API version mechanics, internal limit figures, or other plumbing. Explain only what the user has to decide, consent to, pay for, or act on. Surface the rest only when they ask, when their problem turns on it, or when a reference file names the disclosure as required.

  • MUST ask for missing inputs in ONE question set rather than one at a time — and MUST NOT ask for inputs the request does not need. Scope the questions to what the user asked for: a request that already names the domain and MAIL FROM subdomain is a complete domain-setup request, so read state and execute rather than stalling for send-time or production-access inputs. If nothing is missing for the chosen scope, ask nothing.

  • MUST NOT ask which AWS CLI profile or Region to use while either can still be resolved. Honour --profile only if the user names one. Resolve the Region in this order, taking the first that yields a value: a Region the user named; AWS_REGION; AWS_DEFAULT_REGION; aws configure get region, adding --profile '{PROFILE}' when the user named a profile, since a profile can carry its own Region. That last command reads the CLI configuration files only and does not see the environment, which is why the two environment variables are checked separately and ahead of it. State the resolved Region before any mutation, because SES state is per-Region, and report the account and principal from aws sts get-caller-identity. Two cases, and only these two, are where you do ask: nothing in that chain yields a Region, because every aws sesv2 call fails without one; and aws sts get-caller-identity fails on expired or invalid credentials, in which case ask which account (profile) and Region to use once they are refreshed, because the configured defaults are no longer trustworthy. Fold either into the one question set rather than spending an extra turn on it.

  • MUST pass request payloads (change batches and message content) to the CLI as inline JSON strings, never as file:// paths — file:// is AWS-CLI-specific and does not resolve when the CLI is executed through the AWS MCP server.

  • MUST read current state before each step and skip steps already satisfied. In particular: never call create-email-identity for an identity that already exists (it returns AlreadyExistsException), and never re-run put-email-identity-dkim-signing-attributes on an identity whose DkimAttributes.Status is SUCCESS, because re-initialising can change its tokens. Gate that call on status, not on whether the published CNAMEs resolve — on a FAILED identity the records often do resolve and are simply not being honoured, which is what FAILED means, so resolving records are not a reason to withhold the recovery. The status-gated recovery paths, and the BYODKIM check that precedes them, are owned by setting-up-ses-domain-identity.md.

  • For domain setup, create a domain identity. Create an email-address identity only when the user explicitly chooses verification of that individual address.

Invariants

These hold everywhere in this skill. The reference files apply them, and may add workflow-specific detail on top of them — a reference may state how an invariant is checked at a particular step, or narrow it for that step's state. None of them may contradict or relax what is written here.

  • Domain setup complete means all three of these are true in one aws sesv2 get-email-identity response: VerifiedForSendingStatus is true, and DkimAttributes.Status is SUCCESS, and MailFromAttributes.MailFromDomainStatus is SUCCESS. Two of the three is not complete. While MailFromDomainStatus is PENDING, FAILED or TEMPORARY_FAILURE, AWS documents that SES uses the custom MAIL FROM fallback setting: with USE_DEFAULT_VALUE it sends using a subdomain of amazonses.com, so SPF validates but the envelope sender does not align with the From domain and DMARC cannot pass on its SPF leg; with REJECT_MESSAGE SES returns MailFromDomainNotVerified and does not attempt delivery. This is the gate the rest of the skill calls "domain setup complete".

  • Sandbox recipient rule. While ProductionAccessEnabled is false, AWS documents that you can only send to verified email addresses and domains, or to the Amazon SES mailbox simulator. That means, in the order this skill always presents them: (1) an address verified as its own email identity, (2) the Amazon SES mailbox simulator, or (3) any address at any verified domain in the account — not only the domain just set up. The restriction is on the recipient, not the sender: a fully verified sending domain does not lift it, and you must never attempt to work around it. Production access is governed by ProductionAccessEnabled alone — while it is false, these three options apply whatever Details.ReviewDetails.Status says. When you explain the restriction to a user, you MUST name all three recipient options that work right now, in that same numbering, before proposing production access as the remedy, and you MUST NOT describe the restriction as "each recipient must be individually verified": a verified domain covers every address at that domain.

  • The sender must be verified too, in every account state. AWS documents that after an account moves into production "you still have to verify all identities that you use as 'From', 'Source', 'Sender', or 'Return-Path' addresses." So MessageRejected: Email address is not verified has two causes: an unpermitted recipient in the sandbox, and an unverified sending identity in any state. Read the address named in the error before choosing a fix, and check that the From address is at a domain or address verified in this Region.

  • A verified sender is the SES service minimum; this skill's journey asks for more. SES itself will accept a send from an identity that is verified for sending even when DKIM has not reached SUCCESS. This skill deliberately does not stop there: its goal is a delivered, authenticated first email, so it requires DkimAttributes.Status: SUCCESS before it claims domain setup or authentication is complete, and before it sends. That stricter bar is stated once, as the send prerequisite in onboarding.md's domain-verification step, and every send path applies it. The two are not in conflict — one is what the service enforces, the other is what this journey promises the user.

  • Validate every substituted value, then quote it for its context. A value interpolated directly into a shell command is single-quoted; a value placed inside inline JSON is JSON-encoded (double quotes, per JSON rules), and only the whole JSON blob is single-quoted for the shell — never shell-quote an individual value inside JSON. This applies to every user-supplied value this skill substitutes — DOMAIN, the MAIL FROM subdomain, FROM_ADDRESS, TEST_RECIPIENT, CHOSEN_RECIPIENT, MAIL_TYPE, WEBSITE_URL, REGION, PROFILE — not only the DNS names. Reject any value containing a single or double quote, a backtick, $, ;, a backslash, or whitespace, and ask the user to re-provide it; never escape a rejected value into shape. Per-value shapes: DOMAIN and the MAIL FROM subdomain — DNS labels only, letters, digits, hyphens and dots; From address and every recipient value, TEST_RECIPIENT and CHOSEN_RECIPIENT alike — a single address with one @, and reject a comma-separated list (put multiple recipients in the ToAddresses array instead); MAIL_TYPE — exactly TRANSACTIONAL or MARKETING; WEBSITE_URL — an http/https URL of 1–1000 characters, which need not be at the verified domain, since any business site the mail relates to is acceptable to AWS, so the question set may offer https://{DOMAIN} as a suggested default only; REGION — an AWS Region code; PROFILE — a CLI profile name. A CHOSEN_RECIPIENT bound from an option the skill supplies rather than the user — the mailbox simulator address — is validated the same way. JSON-encode any user-supplied subject or body text with a serialiser rather than pasting it between quotes.

IAM Permissions

Grant only the actions the workflow being run actually calls, and nothing more — never ses:* or *FullAccess. Each reference file has a Required IAM actions section listing exactly what it calls; use that list, not a wildcard.

Scope per-identity actions to the identity each call actually acts on:

  • arn:aws:ses:{region}:{account-id}:identity/{domain} for domain operations.

  • arn:aws:ses:{region}:{account-id}:identity/{address} for an email-address identity — the identity used for sandbox recipient verification, and equally the From identity when the sender is a separately verified email address rather than an address at the domain. A policy scoped only to the domain denies those calls. Reads are scoped the same way: the ses:GetEmailIdentity statement must carry the ARN of every identity actually read, which includes a recipient address identity and — for sandbox recipient option 3 — the exact identity ARN of the other verified domain the user names. A policy scoped only to the sending domain denies that read, which reads as a broken permission rather than as a missing recipient. A send is authorized by the identity that covers its From address, so scope ses:SendEmail to whichever identity that is; the three cases are listed in onboarding.md's Required IAM actions.

  • Route 53 zone actions (route53:GetHostedZone, route53:ListResourceRecordSets, route53:ChangeResourceRecordSets) to arn:aws:route53:::hostedzone/{hosted_zone_id}, using the bare zone ID — list-hosted-zones-by-name returns /hostedzone/Z123ABC, so strip that prefix before building the ARN or the result is double-prefixed and matches nothing. The zone reads are needed whenever the agent checks whether Route 53 hosts the domain; only route53:ChangeResourceRecordSets is conditional on the user approving a write.

Only genuinely account-level and list actions (sts:GetCallerIdentity, ses:GetAccount, ses:ListEmailIdentities, route53:ListHostedZonesByName, route53:TestDNSAnswer) have no resource-level scoping and must be granted on *. Grant ses:PutAccountDetails deliberately, because it changes account-wide sending posture.

Security Considerations

  • Ephemeral credentials. Use IAM roles with STS — never long-lived access keys.

  • Consent gates. Opening a production-access review and sending a real message both commit the user in ways no API can undo, and a Route 53 change can affect live traffic. Each gate — the production-access consent gate, the send confirmation, and the DNS-write permission — is owned by the reference file that performs the action; never proceed past one because this file summarises it.

  • Delivery TLS is opportunistic by default. AWS documents that SES always attempts a secure connection to the receiving mail server and sends the message unencrypted if it cannot establish one, and that requiring TLS means setting a configuration set's TlsPolicy to REQUIRE — a configuration-set workflow outside this skill's scope.

  • Validate and quote every user-supplied value before it enters a shell command or inline JSON. The rules and character sets are under Invariants above.

  • Never place message bodies or recipient lists in logs.

  • Never hardcode credentials, endpoints, or secrets in examples. Store any application credentials in AWS Secrets Manager or Parameter Store.

  • Enable CloudTrail for SES API call auditing, and alarm on repeated AccessDeniedException and unusual send volume. Encrypt the trail's log files with an AWS KMS key (SSE-KMS) and restrict access to the trail's S3 bucket and CloudWatch Logs group, and encrypt that log group with a KMS key (--kms-key-id on create-log-group) — SES CloudTrail entries carry email addresses, domain names, and account details.

Additional Resources

Files

references/onboarding.md

SES Onboarding: Zero to First Delivered Email

Operations below use AWS CLI syntax. For sandboxed execution, use the AWS MCP Server. The two risks here — sending on someone's behalf, and the commitments in a production-access request — are gated at the steps that perform them. SKILL.md's "Invariants" own the domain-setup-complete gate, the sandbox recipient rule, and the value-validation rules.

Session state

Carry these forward between steps; re-read rather than assume.

ValueFormatSource
REGIONus-east-1user if named, else AWS_REGION, AWS_DEFAULT_REGION, then aws configure get region; asked for only if none of those resolves
PROFILECLI profile name, may be emptyuser only if they name one
DOMAINexample.comuser
MAIL_FROMmail.example.comuser, Step 1
FROM_ADDRESSonboarding@{DOMAIN} default (stated, overridable), at {DOMAIN} or another verified identitydefaulted at Step 1 or Step 5; overridable
TEST_RECIPIENT[email protected]user, collected at Step 1, Step 4 or Step 5
MAIL_TYPETRANSACTIONAL or MARKETINGuser, Step 3
WEBSITE_URLhttps://example.comuser, Step 3
CASE_IDSupport case ID, may be absentget-account → Details.ReviewDetails.CaseId
MAX_24_HOUR_SENDnumber; -1 means unlimitedget-account → SendQuota.Max24HourSend
CHOSEN_RECIPIENT_OPTION1 (verify the mailbox as its own identity), 2 (mailbox simulator) or 3 (existing mailbox at a verified domain)selected in Step 4's "Sandbox recipient options"; consumed in Step 5, which picks its delivery check from it
CHOSEN_RECIPIENTthe address the send goes to; a single address, validated like every other substituted valuein the sandbox, bound in Step 4 from the option chosen — option 1: TEST_RECIPIENT; option 2: [email protected]; option 3: the mailbox the user names at an already-verified domain. With ProductionAccessEnabled: true, from TEST_RECIPIENT if it was collected, never from a null, else asked for in Step 5
PA_DECLINEDtrue, else unsetset true when the user declines production access at Step 3; routing must not re-propose it
MESSAGE_IDreturned by the sendsend-email
ACCOUNT_ID12 digitssts get-caller-identity

If PROFILE is set, append --profile '{PROFILE}' to every command; all sesv2 commands also take --region '{REGION}'. Every substituted value is validated and single-quoted first.

Required IAM actions

This list covers only the actions this file calls. The domain-setup leg is owned by setting-up-ses-domain-identity.md, which has its own list; the full journey needs the union, because Step 2 hands the DNS and identity-creation work to that file. Scope as SKILL.md describes; never a wildcard.

  • Read, always needed: sts:GetCallerIdentity, ses:GetAccount, ses:ListEmailIdentities, ses:GetEmailIdentity. Scope the ses:GetEmailIdentity statement to every identity this file reads, not only identity/{DOMAIN}: Step 4 reads identity/{CHOSEN_RECIPIENT} for option 1, and for option 3 the exact identity ARN of the other verified domain the user names. A policy scoped only to the sending domain denies those reads, and the failure looks like a missing recipient rather than a missing permission.

  • Write, only for the scope actually being run. A read-only or domain-setup-only request needs none of the three writes below, though domain setup still needs the other file's writes:

    • ses:PutAccountDetails — only when requesting production access (Step 3).

    • ses:SendEmail — only when a first or test send is in scope (Step 5).

    • ses:CreateEmailIdentity — two distinct scopes, and only one belongs here. Creating the domain identity is owned by setting-up-ses-domain-identity.md, scoped to identity/{DOMAIN}. What this file calls it for is narrower: verifying an individual address as a sandbox recipient (Step 4, option 1), scoped to identity/{CHOSEN_RECIPIENT}. A policy scoped only to the domain denies that call.

  • Narrow ses:SendEmail so a confused or compromised caller cannot send as anyone from this domain. Scope the statement to the identity that authorizes {FROM_ADDRESS} — three cases: an address at {DOMAIN} (the default and common case) → identity/{DOMAIN}; an address verified as its own email identity → identity/{FROM_ADDRESS}; an address at a different verified domain → that domain's identity ARN. Then pin the sender with the ses:FromAddress condition key:

    {
      "Effect": "Allow",
      "Action": "ses:SendEmail",
      "Resource": "arn:aws:ses:{REGION}:{ACCOUNT_ID}:identity/{DOMAIN}",
      "Condition": {"StringEquals": {"ses:FromAddress": "{FROM_ADDRESS}"}}
    }

    ses:FromAddress is an exact string match, so pin it to the {FROM_ADDRESS} collected for this workflow — pinned elsewhere it denies every send and reads like a broken permission.

How to run this: check, then do

Every step is check, then do: read current state first, and if a step is already satisfied say so in one line (✓ Domain already verified) and move on.

Precedence: identity state is evaluated before review state, and a Details.ReviewDetails row never overrides identity state. The table is ordered to match: account-state rows, then identity rows, then review rows. Two thresholds matter and differ: the full domain-setup-complete gate, which Step 3 requires, and the narrower send prerequisite stated in Step 2. Route to Step 2 before a send only when that prerequisite fails — read it in Step 2, not from a row.

State you readDo this
ProductionAccessEnabled: true and SendQuota.SentLast24Hours > 0Already sending in this Region — SentLast24Hours is account-wide within the Region, so it says nothing about this identity. The user's stated intent decides what happens next. A new domain or Region → Step 2, then Step 5; Steps 3 and 4 are already satisfied. A question about an existing send → do NOT run the journey; route it to Failure modes, or to setting-up-ses-domain-identity.md's "Troubleshooting: DKIM Stuck in PENDING" and "Step 7" sections for stuck DKIM or MAIL FROM. If intent is ambiguous, ask: setting something new up, or debugging an existing send?
ProductionAccessEnabled: true, no sending yetSkip Steps 3 and 4 — the identity rows below still apply, so this row does not skip Step 2. Bind CHOSEN_RECIPIENT from TEST_RECIPIENT if it was collected, never from a null; if not, Step 5 asks. Then Step 5.
No domain identityStart at Step 2.
Domain identity exists and the domain-setup-complete gate passesSkip Step 2.
Domain identity exists, DkimAttributes.Status is not SUCCESSDo NOT create it again — that returns AlreadyExistsException. Go to Step 2, which routes to setting-up-ses-domain-identity.md's Step 2 existing-identity branch and adds only what is missing.
Domain identity exists, MailFromDomainStatus is PENDING, FAILED or TEMPORARY_FAILUREThe full gate has not passed, so Step 3 is unavailable. Whether a send may proceed turns on BehaviorOnMxFailure — apply Step 2's send prerequisite: USE_DEFAULT_VALUE permits it, REJECT_MESSAGE does not. Either way, do not re-create the identity or re-run DKIM signing setup.
Domain identity exists, MailFromAttributes absentNo custom MAIL FROM was ever configured — the field is optional, so this differs from PENDING. The full gate has not passed, so Step 3 is unavailable. Go to Step 2 for the MAIL FROM step only; do not re-create the identity or re-run DKIM signing setup.
Domain identity exists and any other reason leaves the gate unmet — including VerifiedForSendingStatus: false while DKIM and MAIL FROM both read SUCCESSTreat it like the rows above: the gate has not passed, so Step 3 is unavailable. Go to Step 2, and name the field that failed rather than reporting the two that passed. Do not re-create the identity.
Details.ReviewDetails.Status: PENDINGA request is in flight. Do NOT submit another. If Step 2's send prerequisite is not met, go to Step 2 first — a pending review does not give you a verified sender. Once met, Step 4 owns this branch and routes to "Sandbox recipient options".
Details.ReviewDetails.Status: GRANTEDSkip Step 3 — the request has already been decided, so never resubmit. ProductionAccessEnabled alone governs the sandbox: true → confirm with Step 4, then Step 5. Still false → the sandbox applies, so bind a permitted recipient through Step 4's "Sandbox recipient options" before Step 5.
Details.ReviewDetails.Status: DENIEDDo NOT resubmit. Step 4 owns this branch — the Support case at CASE_ID, an absent or closed CaseId, and the route into "Sandbox recipient options" that keeps Step 5 available while the case is pursued.
Details.ReviewDetails.Status: FAILEDAWS did not receive the request. Submitting again at Step 3 is safe, then through Step 4 (the new review is PENDING — bind the recipient there) to Step 5.
Details.ReviewDetails absentNo request has ever been submitted in this Region. Step 3 is safe once the domain gate passes — but only run it if the user wants production access and PA_DECLINED is not set. Otherwise the account is still in the sandbox: go to Step 4's "Sandbox recipient options" to bind a permitted recipient, then Step 5.

Step 1: Preflight — one question set, then read state

Ask in ONE message, and ask only for what the request needs. Do not drip-feed questions; equally, do not block domain setup on inputs belonging to a later step. Never ask for anything already supplied, and never ask for the AWS CLI profile or Region — SKILL.md's Critical Rules own both, including the two credential/Region cases where you do ask. Two ordering requirements here.

First, run aws sts get-caller-identity before composing the question set — credentials found dead afterwards throw the answers away and may have pointed at the wrong account. If it fails, say how to refresh them and ask, in the same message, which account (profile) and Region to use once back.

Second, resolve the Region by SKILL.md's precedence chain before any aws sesv2 call — user-named, AWS_REGION, AWS_DEFAULT_REGION, then aws configure get region (profile-aware). If none yields a value, ask for one in the same question set. Bind it as REGION and state it before any mutation.

Scope the question set to the request:

The user asked for…Ask only forDefer
Domain setup / verification onlysending domain; MAIL FROM subdomaineverything else, until they ask to send
The full journey to a delivered emailsending domain; MAIL FROM subdomain; test recipient (From address is defaulted, not asked)mail type and website URL — collected at Step 3, never at preflight
Production accesssending domain; MAIL FROM subdomainmail type and website URL — collected at Step 3; From address defaulted, test recipient deferred to Step 5. Production access requires the domain gate first, so this routes through Step 2 then Step 3; it is not a shortcut past domain setup.

The From address is never one of the questions. Default it to onboarding@{DOMAIN} — the bare address, with no display name — state it in the same message as the question set, and invite an override in the same breath. Bounce notices arrive there by default, so a real mailbox is worth having. Because it is stated rather than asked, the question set for the full journey carries three items.

Example, for the full journey:

To get you from nothing to a delivered email I need three things:

  1. Sending domain — the domain you want mail to come from, e.g. example.com. You need to be able to add DNS records for it.

  2. MAIL FROM subdomain — a subdomain used as the envelope sender, which is what makes SPF align. Common choices are mail.example.com or bounce.example.com; pick one.

  3. Test recipient — an email address you can actually open, to receive the first message.

MUST NOT ask for mail type or website URL before Step 3 — not even when the user says they want production sending. Production access requires a verified domain first, so asking up front delays the identity creation that has to happen anyway.

If the user overrides the From address, check it against the sending domain. An override not at {DOMAIN} is held until the state read below — an address at another identity already verified in this Region is also legitimate. At no verified identity, say so and ask for one that is.

Then read state before doing anything:

aws sts get-caller-identity
aws configure get region          # last resort only, after AWS_REGION and AWS_DEFAULT_REGION; add --profile '{PROFILE}' if named
aws sesv2 get-account --region '{REGION}'
aws sesv2 list-email-identities --region '{REGION}'

From get-account read ProductionAccessEnabled, Details.ReviewDetails.Status, SendQuota.SentLast24Hours and SendQuota.MaxSendRate, and record CASE_ID and MAX_24_HOUR_SEND — read the account's own limits here rather than asserting a figure later — then route with the table above. Two fields decide whether the journey can run at all: SendingEnabled: false means sending is switched off in this Region and no send will succeed; EnforcementStatus of PROBATION or SHUTDOWN means a reputation problem this workflow does not address (HEALTHY is the good value). Surface either and stop.

From list-email-identities, note whether {DOMAIN} is present. If it is, read it with get-email-identity before deciding anything — presence alone does not mean the gate passes.

Step 2: Verify the sending domain

Create a domain identity with Easy DKIM, add a custom MAIL FROM subdomain for SPF alignment, and publish a DMARC record — then present all DNS records in a single batch.

Do not run any create or put call from here, and do not improvise the record shapes. The whole procedure lives in setting-up-ses-domain-identity.md, which owns the entire DNS leg — including asking the user to add the records and confirm they are live, so do not ask for either a second time here. Follow it, then return carrying whatever its handoff produces.

SES detects the records on its own once they resolve; there is no API to force a re-check. Do not wait open-endedly: re-read the identity once the lookups resolve rather than polling on a timer, and if every record resolves publicly while the status has not moved, go to that file's DKIM troubleshooting section.

Step 2 is complete when the domain-setup-complete gate in SKILL.md's "Invariants" passes — all three of its fields true in one response:

aws sesv2 get-email-identity --email-identity '{DOMAIN}' --region '{REGION}'

The send prerequisite — stated here, and applied rather than redefined elsewhere. A send may proceed when the identity is verified for sending with DkimAttributes.Status: SUCCESS, and either MailFromAttributes.MailFromDomainStatus is SUCCESS or it is unresolved with BehaviorOnMxFailure set to USE_DEFAULT_VALUE. It may not proceed while MAIL FROM is unresolved and BehaviorOnMxFailure is REJECT_MESSAGE: AWS documents that SES then returns MailFromDomainNotVerified and does not attempt delivery. MailFromAttributes absent entirely → no custom MAIL FROM in effect, and this journey routes to Step 2 because custom MAIL FROM is a MUST here. In the USE_DEFAULT_VALUE case SES falls back to a subdomain of amazonses.com as the envelope sender — say plainly that it will not align with {DOMAIN} and DMARC must pass on DKIM alignment alone, then go to Step 5. Never report the domain as complete in either state.

Why DKIM SUCCESS is in that prerequisite when SES itself does not require it. Per SKILL.md's sender invariant, the service minimum is a verified sender; this journey holds the higher bar because it promises a delivered, authenticated first email whose Authentication-Results shows dkim=pass. If the user explicitly wants to send sooner, say what they give up and never silently relax the prerequisite.

Scope gate — check the requested scope before going on to Step 3 or Step 5. A domain-setup-only request ends here: report the result, name any outstanding field, and stop. Do not prompt for production access and do not offer or perform a test send — neither was asked for, and Step 3 opens an AWS Support case while Step 5 sends real mail. Say in one line that both are available on request. Continue only when the request asked for production access, a send, or the full journey.

Step 3: Request production access

MUST confirm the domain-setup-complete gate passes before submitting. AWS documents that "verifying your domain with SES before requesting production access is a best practice that helps to get your production access request approved faster." If the user asks to submit first, explain that and complete the domain first.

MUST get explicit consent before submitting. The console form makes the user tick an acknowledgement box; the API has no parameter for it, so submitting via the CLI makes that commitment on the user's behalf. Say this in one paragraph and wait for a yes:

Submitting this tells AWS you will only send to people who asked for your mail, and that you have a way to handle bounces and complaints. It opens an AWS Support case, and AWS documents that you cannot edit your details until the review completes — there is no API to cancel or withdraw it. Shall I submit it? (You can also fill in the form yourself in the SES console.)

If the user declines, accept it and move on — do NOT re-propose it. Production access is not needed for a first delivered email. Say that plainly, set PA_DECLINED, skip the rest of Step 3 (do not collect MAIL_TYPE or WEBSITE_URL), and go to Step 4's "Sandbox recipient options" to bind a permitted recipient, then Step 5 — the account is in the sandbox, so that binding is required before the send. Do not raise production access again this session unless the user brings it up.

Two inputs are required and collected here. Ask now if the request did not supply them — never invent either. Explain each in one line, because the user is choosing:

  • MAIL_TYPE — TRANSACTIONAL for mail a user's own action triggers, such as password resets and receipts; MARKETING for mail sent to a list, such as promotions and newsletters.

  • WEBSITE_URL — the business site the email relates to; it need not be at the sending domain.

aws sesv2 put-account-details \
  --production-access-enabled \
  --mail-type '{MAIL_TYPE}' \
  --website-url '{WEBSITE_URL}' \
  --region '{REGION}'
  • Do NOT pass UseCaseDescription. The SESv2 API marks that parameter deprecated.

  • Optional: --additional-contact-email-addresses (up to 4) and --contact-language EN|JA. AWS uses those contacts for correspondence about this account, so offer only addresses controlled by authorized members of the user's own team — never an external recipient, a customer address, or a public list.

  • Success looks like nothing. The API returns an empty HTTP 200 — no case ID, no status. That is expected, not a failure. Read the status back in Step 4.

  • Whenever you present or run this command, tell the user two things: how to check the review status afterwards (aws sesv2 get-account, read Details.ReviewDetails.Status), and that they can already send today — naming all three options from Step 4's "Sandbox recipient options", in that numbering.

  • A second call while a review is pending returns ConflictException (HTTP 409); there is no API to cancel or withdraw. BadRequestException (HTTP 400) means an input is invalid, not a refusal — see Failure modes.

Step 4: Check the decision

Read the whole response, not a projection — these branches turn on several nested fields:

aws sesv2 get-account --region '{REGION}'

Read ProductionAccessEnabled at the top level, and Details.ReviewDetails.Status and CaseId if Details.ReviewDetails is present at all — it is optional, and its absence is a branch below.

ProductionAccessEnabled alone decides whether the sandbox applies, so read that field before the review status. While it is false, SKILL.md's sandbox recipient rule and "Sandbox recipient options" below apply whatever Details.ReviewDetails.Status says, GRANTED included. A GRANTED status does still mean the request has been decided, so skip Step 3 and do not resubmit.

ProductionAccessEnabled: true — and only this field — lifts the sandbox restriction, so "Sandbox recipient options" does not apply. Bind CHOSEN_RECIPIENT from the TEST_RECIPIENT collected in Step 1 if there is one, never from a null; if it was never collected, Step 5 asks. Any address works now, so offer to change it. Then Step 5.

ReviewDetails.Status: PENDING — under review. Do not tell the user to wait, and never that the account will be out of review in 24 hours: AWS documents an initial response within 24 hours, grants inside that window only "If we're able to do so", and may take longer. After that window the Support case is where AWS responds — read it as the DENIED branch describes. The account is still in the sandbox, so go to "Sandbox recipient options" below, opening with: "Your request is in. In the meantime you can test everything end to end right now — you just have to send to a recipient AWS already knows about."

ReviewDetails.Status: DENIED — do not resubmit. Read the Support case at CASE_ID. CaseId is optional, so it may be absent — and the case may have been closed. Either way do not stop at "read the case": have the user open the SES account dashboard in the console, which shows the production-access state, and re-engage AWS Support with a new case referencing the denial if they want the decision revisited. This is not the end of the journey: the account is still in the sandbox, not switched off. Go to "Sandbox recipient options" below, opening with: "The review came back denied — nothing about sandbox testing is lost."

ReviewDetails.Status: FAILED — AWS did not receive the request; it is safe to submit again at Step 3. The resubmitted review is PENDING, which returns here.

Details.ReviewDetails absent — no request has been submitted in this Region. If the user wants production access, Step 3 is the next action and it is not a resubmit. If they do not, or PA_DECLINED is set, do not send them back to Step 3 — go to "Sandbox recipient options" below, opening with: "You're still in the SES sandbox, so the first send has to go to a recipient AWS already knows about."

Sandbox recipient options

This subsection is state-neutral. It applies whenever ProductionAccessEnabled is false — on all six of these paths: review PENDING; review DENIED; review GRANTED while ProductionAccessEnabled is still false; review FAILED where the user declines to resubmit at Step 3; PA_DECLINED set at Step 3; and Details.ReviewDetails absent. Where a branch above supplies its own opening sentence, use it; the options, numbering and binding rules are identical on every one of the six. No sandbox path reaches Step 5 without binding CHOSEN_RECIPIENT here and reading it back.

Ask which option they want, and record CHOSEN_RECIPIENT_OPTION and the address it resolves to as CHOSEN_RECIPIENT:

Pick whichever fits:

  1. Verify the mailbox you want to test with, as its own email identity — recommended. AWS sends a verification message to that address; open it and click the link, which expires 24 hours after the message was sent. This is the option that lets you read the delivered message and check its authentication headers.

  2. [email protected] — the Amazon SES mailbox simulator. Nothing lands in a real inbox, so it is a fast check of the send path only, with no authentication headers to read. It works while the account is in the sandbox, is billed like any other send, and does not count against the daily sending quota (the sending rate still applies).

  3. A real mailbox at a domain already verified in this account — only if such a mailbox actually exists. Verifying a domain in SES creates no mailboxes and no MX records, so an address at the domain you just set up receives nothing unless that domain already has real mail hosting. If it does, name that mailbox; no extra verification is needed.

Set CHOSEN_RECIPIENT per the session-state table and read it back before Step 5. Step 5 sends to CHOSEN_RECIPIENT, not the Step 1 answer.

Inputs differ by option. Option 1 needs an address the user can open — if TEST_RECIPIENT was never collected, ask for it in the same message as the option choice, so it stays one question. Option 2 needs no address. Option 3 needs a real mailbox at a verified domain. Never invent an address, never derive one from {DOMAIN}, and never substitute the simulator for an option the user did not choose.

Option 1 is check-then-do. Read the identity before creating it:

aws sesv2 get-email-identity --email-identity '{TEST_RECIPIENT}' --region '{REGION}'
  • NotFoundException → create it, which sends the verification message: aws sesv2 create-email-identity --email-identity '{TEST_RECIPIENT}' --region '{REGION}'

  • Exists with VerifiedForSendingStatus: true → say so in one line and go to Step 5.

  • Exists with false → the verification link is outstanding. Do not re-create it.

Then gate the send on the verification completing. The user clicks the link out of band, so read the state back and do not call send-email until it is true:

aws sesv2 get-email-identity --email-identity '{TEST_RECIPIENT}' --region '{REGION}' \
  --query 'VerifiedForSendingStatus'

If still false, wait and re-read; sending first returns MessageRejected, which looks like a generic sandbox restriction and is not one. Bound that wait rather than looping: have the user check that mailbox's spam folder, and remember that AWS documents the link expiring 24 hours after the message was sent — that window is the escape point, not an interval to poll through. There is no resend here: re-running create-email-identity returns AlreadyExistsException and sends no second message, and SendCustomVerificationEmail is a separate templated feature this skill does not use. An expired link is handled from the SES console, per Failure modes.

Option 3 is check-then-do too — verify the domain rather than trusting the name. The user names the mailbox; confirm the account can send to it with aws sesv2 list-email-identities --region '{REGION}':

  • The domain part of the named address must appear in that list as an identity in this Region. If not, this option does not apply: say so and have the user pick option 1 or 2.

  • Read that identity's state before relying on it (aws sesv2 get-email-identity --email-identity '{that-domain}' --region '{REGION}'). An unverified domain identity does not make its addresses permitted recipients.

  • Whether a mailbox exists there is not something any API can tell you. Confirm it with the user in words.

While ProductionAccessEnabled is false, SKILL.md's sandbox recipient rule applies, per Region. If the user asks about limits, answer from the SendQuota values read in Step 1 rather than asserting a figure, and note that AWS counts that quota by recipients, not messages. A MAX_24_HOUR_SEND of -1 means the daily quota is unlimited, so report it that way. Do not raise limits unprompted.

Step 5: Send the first email

A send must have been asked for — Step 2's scope gate stops a domain-setup-only request.

Apply Step 2's send prerequisite first, re-reading the identity rather than trusting a carried value — DKIM may have moved since:

aws sesv2 get-email-identity --email-identity '{DOMAIN}' --region '{REGION}'

If it is not met — DkimAttributes.Status not SUCCESS, or MAIL FROM unresolved with BehaviorOnMxFailure at REJECT_MESSAGE — do not send: name the outstanding field and go back to Step 2. In the sandbox a permitted recipient does not substitute for this; the recipient rule and the send prerequisite are separate gates and both apply.

Then bind the sender and recipient. If {FROM_ADDRESS} is not bound, default it to onboarding@{DOMAIN} and STATE it with Step 1's override invitation — do not ask. If {CHOSEN_RECIPIENT} is not bound — a domain-setup-only request defers it — ask for it now, in ONE question, at the point of use; in the sandbox it must be bound by Step 4's "Sandbox recipient options" rather than asked for freely, because an arbitrary address is not a permitted recipient there. Never invent a recipient: the From default is the one derived value this skill permits, because any mailbox at the verified domain is a legal sender.

{FROM_ADDRESS} must be at {DOMAIN} or at another identity verified in this Region, per SKILL.md's sender invariant; if it is not, ask for an address at {DOMAIN} rather than sending.

MUST ask before sending. A real message is about to reach a real person:

The next command sends a real email from {FROM_ADDRESS} to {CHOSEN_RECIPIENT}. Ready?

Use aws sesv2 send-email, never the v1 form, per SKILL.md's Critical Rules.

aws sesv2 send-email \
  --from-email-address '{FROM_ADDRESS}' \
  --destination '{"ToAddresses":["{CHOSEN_RECIPIENT}"]}' \
  --content '{"Simple":{"Subject":{"Data":"SES first send check"},"Body":{"Text":{"Data":"If you are reading this, Amazon SES is set up correctly."}}}}' \
  --region '{REGION}'

--feedback-forwarding-email-address sets where SES sends bounce and complaint notifications; left off, they go to {FROM_ADDRESS}. If that is not a monitored mailbox, set it to a deliverable address at the verified {DOMAIN} controlled by an authorized member of the user's own team — bounce notifications carry recipient addresses, so never an external recipient or public list. Omit the flag rather than guessing. SES is billed per recipient — see Amazon SES pricing — so say that before sending.

A returned MessageId means Amazon SES accepted the message — not that it was delivered. Say that plainly, then run the check matching CHOSEN_RECIPIENT_OPTION:

SituationHow you know it worked
ProductionAccessEnabled: true, or option 1 (the mailbox just verified), or option 3 (a real mailbox at a verified domain)Open the message and view its raw headers. Find Authentication-Results and confirm dkim=pass, spf=pass and dmarc=pass. This is the only check that proves authentication.
Option 2, the mailbox simulator[email protected] has no inbox, so there is no Authentication-Results header. A MessageId proves the send path only and proves nothing about DKIM, SPF or DMARC. Say so, then send the user back to Step 4's "Sandbox recipient options" to pick option 1 or option 3 — a mailbox they can open — re-bind CHOSEN_RECIPIENT_OPTION and CHOSEN_RECIPIENT there, and return here. Do not call authentication confirmed until that second send's headers have been read.
Option 3, but nothing arrives because no real mailbox exists thereVerifying a domain creates no MX records and no mailboxes, so authentication is unproven. Go back to "Sandbox recipient options", pick option 1, re-bind CHOSEN_RECIPIENT there and return here.

Where raw headers live differs by mail client — in Gmail, More (three vertical dots) then Show original. dmarc=pass appears only if a _dmarc TXT record is published for {DOMAIN} — a precondition, not automatic; if DMARC is missing or shows none, go back to the DMARC step in setting-up-ses-domain-identity.md.

If it has not arrived: check the spam folder first, then see Failure modes. A bounce, if there is one, arrives by email at the feedback-forwarding address.

Step 6: After it arrives — optional extras

Only offer these once the test send has landed. Offer once, act on a yes, do not push. Dedicated IPs and tenants exist for higher volume and for isolating senders — one line each. Do not raise pricing plans.

Failure modes

SymptomCauseFix
MessageRejected: Email address is not verifiedTwo causes — read ProductionAccessEnabled and the address named in the error before choosing. false and the named address is the recipient: the sandbox restriction is on the recipient, and a verified sending domain does not change it. Otherwise the unverified identity is the sender, which must be verified in production too.Recipient case: go to Step 4's "Sandbox recipient options" and re-bind CHOSEN_RECIPIENT. Production access removes the restriction but is not the only fix. Sender case: verify the named From or Return-Path identity in this Region, or send from an address at {DOMAIN}. If that identity is verified but its DkimAttributes.Status is not SUCCESS, this is not the fix — re-apply Step 2's send prerequisite and complete DKIM first, because this journey's bar is an authenticated message, not merely an accepted one.
MessageRejected right after verifying a recipient addressThe verification link has not been clicked.Read VerifiedForSendingStatus on that identity and wait for true — Step 4's gate.
AlreadyExistsException on create-email-identityThe identity already exists in this Region.Read it with get-email-identity and add only what is missing. Never create over an existing identity.
ConflictException on put-account-detailsA review is in flight.Read Details.ReviewDetails.Status. Do not resubmit.
BadRequestException on put-account-detailsAn input is invalid — not a refusal.MailType must be MARKETING or TRANSACTIONAL; WebsiteURL 1–1000 characters; at most 4 extra contacts; ContactLanguage EN or JA. Fix and resubmit.
MailFromDomainStatus: FAILED or TEMPORARY_FAILUREFAILED is terminal — AWS documents that SES then "no longer attempts to detect the required MX record", so re-reading never clears it. TEMPORARY_FAILURE means SES could not determine the status and is still searching.Both belong to the MAIL FROM step in setting-up-ses-domain-identity.md: confirm exactly one MX record on the subdomain in the authoritative zone, then restart the setup there for FAILED only. Do not re-create the identity, and do not re-call put-email-identity-mail-from-attributes with unchanged values.
ThrottlingException (HTTP 400), Daily message quota exceededSends in the last 24 hours reached SendQuota.Max24HourSend.Compare SentLast24Hours against Max24HourSend — unless Max24HourSend is -1, which means the daily quota is unlimited: the comparison does not apply and this is not the cause, so look at MaxSendRate instead. With a real limit, the quota is computed over a rolling 24-hour window, so it clears only as that window advances — do not present production access as an instant fix.
ThrottlingException (HTTP 400), Maximum sending rate exceededSends exceeded SendQuota.MaxSendRate.Reduce the send rate and send one recipient per SendEmail call; AWS documents waiting an interval (up to 10 minutes) and then retrying.
MessageId returned but nothing arrivedAccepted ≠ delivered.Check spam, then the domain's DMARC policy, and read the bounce notification SES forwards to the feedback-forwarding address.
Header shows dmarc=none, dmarc=fail, spf=fail/neutral, or spf=pass with DMARC's SPF leg failingMissing or misaligned DNS, not a send problem: no _dmarc record; a From domain aligning with neither the DKIM d= domain nor SPF; an SPF record on the MAIL FROM subdomain that is missing, wrong or duplicated; or no custom MAIL FROM, so the amazonses.com envelope sender does not align.All are fixed in setting-up-ses-domain-identity.md — its DMARC step for _dmarc.{DOMAIN}, its MAIL FROM step for SPF and envelope alignment. Also send from an address at the verified domain so d= matches. DKIM alignment alone can satisfy DMARC, so a failing SPF leg is not necessarily fatal.
AccessDeniedExceptionMissing ses: permissions.Compare the caller's policy against "Required IAM actions" above, plus setting-up-ses-domain-identity.md's for the domain-setup leg.
Works in one Region, fails in anotherIdentities, production access and quotas are per-Region.Verify the domain and request production access in each Region you send from.
Verification email link does not workAWS documents the link expiring 24 hours after the message was sent.Request a new verification message from the SES console identity details page; there is no SESv2 resend operation.
Every send fails regardless of recipientSendingEnabled: false for the account in this Region.Read aws sesv2 get-account. This workflow cannot change that account-level state; contact AWS Support.

Requirements and compatibility

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Needs: Needs an MCP server

Security

No issues found by our static scan· No issues found

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Published by
AWS
Publisher
AWS (Official)
Source
aws/agent-toolkit-for-aws · skills/specialized-skills/customer-communications-skills/amazon-ses · commit 0a218f8
Licence
Apache License 2.0 (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE)
  • NOTICE (added by AutoGineer from NOTICE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by AWS.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download amazon-ses.zip