Skip to content

AWS SES

[endpoints.transport]
type = "ses"
[endpoints.transport.settings]
access_key_id = "${env.AWS_ACCESS_KEY_ID}"
secret_access_key = "${env.AWS_SECRET_ACCESS_KEY}"
region = "us-east-1"
SettingRequiredDescription
access_key_idyesAWS access key ID (operator-facing identifier; e.g., AKIA...). Appears in the SigV4 Authorization header.
secret_access_keyyesAWS secret access key. Used as the signing secret. Never sent on the wire; never logged.
regionyesAWS region (e.g., us-east-1, eu-west-1). SES is region-scoped; the endpoint is per-region.
base_urlnoEndpoint override. Default is https://email.<region>.amazonaws.com. Test-only escape hatch.

The IAM user or role whose credentials you configure needs:

ses:SendEmail (or ses:SendRawEmail; SendEmail covers our v1.0 surface)

scoped to the verified sender identities in the account. A minimal policy:

{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["ses:SendEmail"],
"Resource": ["arn:aws:ses:us-east-1:<account-id>:identity/yourdomain.com"]
}]
}
AspectBehavior
API endpointPOST https://email.<region>.amazonaws.com/v2/email/outbound-emails
AuthenticationAWS Signature Version 4 (SigV4) — bespoke implementation, no AWS SDK
Body formatJSON (SESv2 SendEmail shape: FromEmailAddress, Destination.ToAddresses, Content.Simple.Subject.Data, Content.Simple.Body.Text.Data)
Per-request timeout5s (transport-level)
transport_message_idParsed from response MessageId field
SES responseError classRetry?
200 OK(success)no
429 Too Many Requests (Throttling)ErrRateLimitedyes, after 5s
5xx server errorErrTransientyes, after 1s
4xx other than 429 (MessageRejected, MailFromDomainNotVerified, etc.)ErrTerminalno

SES error responses come in two shapes ({"__type": "...", "message": "..."} and {"Message": "..."}); Posthorn checks both.

New SES accounts start in sandbox mode, which restricts sending to verified recipients. Production access requires a service-limit increase request:

  • AWS console → SES → Account dashboard → “Request production access”
  • Fill the form (use case, opt-in process, bounce handling)
  • Approval typically 24–48 hours

In sandbox, sending to an unverified recipient returns MessageRejected: Email address is not verifiedErrTerminal.

SES requires you to verify the sender identity in your AWS account — either:

  • Domain identity — DKIM CNAMEs published from AWS console; SPF via include:amazonses.com. Recommended.
  • Email-address identity — per-address verification email. Easier for one-off senders but doesn’t scale.

The IAM policy above must scope to the verified identity ARN.

SymptomLikely causeFix
4xx MessageRejected: Email address not verifiedSandbox mode + unverified recipientRequest production access, or verify the test recipient first
403 SignatureDoesNotMatchClock skew — SigV4 timestamps must be within 15 minutes of AWS’s clockSync the Posthorn host’s clock (NTP)
403 InvalidClientTokenIdAccess key ID wrong, or IAM user disabledCheck key in AWS console; rotate if compromised
Mail goes to recipient’s spamDKIM CNAMEs not published, or DMARC alignment wrongVerify DKIM via AWS console’s verifier; publish DMARC p=none initially