Amazon SES: "User Is Not Authorized to Perform ses:SendEmail" (The IAM Fix)
You ran your code. Nothing sent. Instead you got this:
User: arn:aws:sts::123456789012:assumed-role/my-app-role/… is not authorized to perform: ses:SendEmail
Here’s the good news: your code is fine. This is a permissions error, and it’s a five-minute fix — once you know the two things almost everyone gets wrong.
First: What This Error Actually Means
This is an IAM error, not a code error and not a verification error. The identity your application runs as doesn’t have permission to call SES.
AWS denies everything by default. Nothing can do anything until a policy explicitly allows it. So “not authorized to perform ses:SendEmail” is AWS telling you, precisely, which action got blocked — and it’s up to you to grant it.
The Fix: One Policy
Attach a policy that allows ses:SendEmail — and ses:SendRawEmail too, if you send raw MIME or attachments through older SDKs:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ses:SendEmail",
"ses:SendRawEmail"
],
"Resource": "*"
}
]
}
One note on those two actions: ses:SendEmail covers both the v1 and v2 SendEmail APIs, and ses:SendRawEmail covers raw MIME and older SDKs. Those two together is the right pair for almost every app.
You can either paste this JSON in IAM’s policy editor, or build the same thing in the visual editor (pick SES as the service, check SendEmail and SendRawEmail). The JSON is just faster and unambiguous.
The Part Everyone Gets Wrong: Attach It to the Right Identity
This is where most of the wasted hours go. The policy has to go on the identity your app actually runs as — not your personal IAM user.
- On Lambda, that’s the function’s execution role.
- On EC2, it’s the instance role.
- In a container (ECS/Fargate), it’s the task role.
And you don’t have to guess which one — the error names it. Read the ARN:
…assumed-role/my-app-role/…
That’s the identity that got denied. Attach the policy to my-app-role, and the error goes away.
Keep It Least-Privilege
It’s tempting to reach for the managed AmazonSESFullAccess policy and move on. Don’t. That grants far more than sending — it’s the opposite of least-privilege.
Two actions is all you need. If you want to lock it down further, scope the policy to a single verified identity and even a specific From address:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["ses:SendEmail", "ses:SendRawEmail"],
"Resource": "arn:aws:ses:us-east-1:123456789012:identity/yourdomain.com",
"Condition": {
"StringEquals": { "ses:FromAddress": "noreply@yourdomain.com" }
}
}
]
}
Swap in your account ID, region, domain, and From address.
The One Thing Nobody Tells You: Permissions and the Sandbox Are Two Different Gates
People constantly conflate this error with the SES sandbox. They’re not the same thing, and confusing them sends you fixing the wrong one.
- This error is about whether you’re allowed to send at all — an IAM permissions question.
- The sandbox is about who you’re allowed to send to — verified addresses only, and a low daily cap.
They’re independent gates. Fixing your permissions won’t get you out of the sandbox. Leaving the sandbox won’t grant your role permission. If you’re hitting both walls, you fix them separately — the policy above for one, a production-access request for the other.
Quick Checklist
- Read the ARN in the error — that names the identity.
- Attach a policy allowing
ses:SendEmail(andses:SendRawEmail). - Put it on your app’s role, not your personal user.
- Keep it to those two actions — skip
AmazonSESFullAccess. - Still blocked after that? Check you’re operating in the right region and that the identity is verified — separate errors, same discipline: read what AWS is telling you and fix that exact thing.
The Pattern Underneath
Nearly every “SES won’t send” problem is AWS denying something by default and telling you precisely what. The fix is almost always the same two-part question: which identity, and which action. Read the error, answer those two, and you’re sending.