Amazon SES Production Access — Use Case
2026-05-31 update: this document was refreshed after the AP-CC-6-002 follow-up work shipped (functional self-service unsubscribe form, RFC 8058 List-Unsubscribe header on every send, per-recipient HMAC tokens, suppression-list IAM grants). The wording reflects the actual deployed state on prod account 943273444105.
Service profile
- AWS account: 943273444105 (KeyNova production)
- Region: ap-south-1 (Mumbai)
- Sender identity:
mail.keynova.ai(verified domain; Easy-DKIM RSA 2048; custom MAIL FROMbounce.mail.keynova.ai; SPF + DMARC published; DMARCp=nonewhile warming) - Sending application: KeyNova, a multi-tenant hotel-management SaaS operated by CentraLogic Solutions Pvt Ltd.
- Corporate website: https://keynova.ai (hosted on AWS S3 + CloudFront in the mgmt account; migrated from Hostinger 2026-05-30)
Recipients and consent
Every recipient on the platform falls into one of two consent buckets:
- Guests of tenant hotels. Recipients who have completed a reservation at a hotel that uses KeyNova. Reservation completion constitutes transactional consent for booking confirmations, OTPs, payment receipts, e-invoices, pre-arrival, mid-stay, and check-out communications.
- Tenant staff users. Recipients who have been explicitly provisioned in the platform by a tenant administrator. Staff users receive only operational and account-management email (password reset, account setup, night-audit summaries, system alerts).
We do not send to purchased, rented, scraped, or otherwise non-consenting recipient lists. Recipient lists are never shared between tenants.
Email types (transactional only)
The platform sends only transactional email. No marketing, no promotional, no bulk-broadcast.
- Booking confirmations and modifications
- Pre-arrival, mid-stay, check-out communications
- Payment receipts and e-invoices (with GST IRN)
- One-time passwords (OTP)
- Password reset and account-setup links
- Night-audit summary reports for hotel staff
- System-generated operational alerts to tenant administrators
Volume
- Initial target: ~5,000 emails/day
- Peak headroom requested: ~25,000 emails/day
- Daily peak hour: 50% of daily volume between 09:00–13:00 IST (booking-confirmation traffic)
Bounce, complaint, and suppression handling
Every send routes through the SES configuration set keynova-prod, whose event destination is the SNS topic arn:aws:sns:ap-south-1:943273444105:keynova-prod-ses-events. The topic HTTPS-subscribes to our notification service at https://api.keynova.ai/api/email/webhook/ses. The webhook handler is signature-verified (sns-validator library), topic-ARN allow-listed, and idempotent on Prisma upsert.
- Hard bounce (
bounceType=Permanent) — recipient is automatically opted out at the platform layer (NotificationPreference.optedOut=true) AND added to the SES account-level suppression list once production access is granted. - Complaint — same as hard bounce: platform opt-out + SES account-level suppression.
- Soft bounce (
Transient) — no opt-out; logged for observability.
Suppression is enforced cross-tenant — once opted out, no tenant on the platform can email the recipient again. The pipeline has been smoke-verified end-to-end across all four environments (test, qa, staging, prod): a real SES Delivery event flows SES → SNS → controller, signature verifies, and downstream side-effects (NotificationLog status update, opt-out upsert) execute correctly.
Unsubscribe mechanism — three independent paths
KeyNova provides three independent paths for a recipient to opt out, far exceeding the CAN-SPAM / DPDPA baseline:
1. Per-email unsubscribe link (every send)
Every outbound transactional email carries a per-recipient HMAC-signed unsubscribe token in:
- An RFC 8058 one-click
List-Unsubscribeheader (List-Unsubscribe: <https://keynova.ai/unsubscribe?token=...>) - A
List-Unsubscribe-Post: List-Unsubscribe=One-Clickheader (enables Gmail/Outlook native one-click unsubscribe buttons) - A visible footer link in the HTML body pointing to the same URL
Clicking the link (or the email client's native unsubscribe button) opts the recipient out across the entire platform regardless of which tenant initiated the send.
2. Self-service unsubscribe form (public website)
A functional form at https://keynova.ai/unsubscribe lets anyone enter an email address (with optional reason) and submit. The form POSTs to https://api.keynova.ai/api/preferences/unsubscribe-by-email and:
- Adds the address to the SES account-level suppression list (
ses:PutSuppressedDestinationwith reason=COMPLAINT) — once production access is granted (the API call currently returns "Your account is still in the sandbox" from SES, and the code already handles this gracefully). - Flips
NotificationPreference.optedOut=truefor any guest row in our database with a matching email, scoped by NotificationLog history so we only opt-out for tenants we have a real send relationship with.
Rate-limited at the API gateway (3 requests / minute / IP) to prevent abuse.
3. Manual escalation
For non-guest recipients (e.g. staff with expired tokens, or a complete bypass case), abuse@keynova.ai is monitored with one-business-day SLA documented in the public Anti-Spam Policy.
Multi-tenant model
The platform is multi-tenant but sends from a single verified domain. Per-tenant vanity From addresses are not enabled in this release. Display names carry the tenant's brand (e.g. Lotus Resort <no-reply@mail.keynova.ai>); the From address is always controlled by KeyNova. We do not resell, sublicense, or expose SES capacity to third parties.
Public policy surfaces (live on keynova.ai)
- Acceptable Use Policy: https://keynova.ai/aup — defines prohibited use, account responsibility, reporting violations procedure, and termination policy
- Anti-Spam & Email Policy: https://keynova.ai/anti-spam — transactional-only commitment, consent basis, automated bounce/complaint handling, self-service unsubscribe details, RFC 8058 one-click, sender identity, abuse reporting
- Privacy Policy: https://keynova.ai/privacy — includes a dedicated "Email handling & AWS SES" section covering lawful basis, sub-processor disclosure (AWS SES in ap-south-1), retention (90 days post-checkout per DPDPA), cross-border data transfer (data stays in India), and DPDPA rights (access, rectification, erasure, portability)
- Unsubscribe: https://keynova.ai/unsubscribe — functional self-service form (not just informational)
- Abuse contact:
abuse@keynova.ai(1 business day SLA) - Privacy contact:
privacy@keynova.ai
All policy pages are hosted on AWS CloudFront in the mgmt account (apex DNS cut over from Hostinger to CloudFront on 2026-05-30, S3 origin with OAC).
Domain authentication evidence
- DKIM: three CNAME records published in
keynova.aizone formail.keynova.ai; SES identity status Verified. - SPF:
v=spf1 include:amazonses.com ~allpublished atbounce.mail.keynova.ai(the custom MAIL FROM subdomain — ensures SPF aligns with the From header for DMARC). - DMARC:
v=DMARC1; p=none; rua=mailto:dmarc-reports@keynova.ai; fo=1;published at_dmarc.mail.keynova.ai. Tightening top=quarantineafter 14 days of clean aggregate reports. - Custom MAIL FROM:
bounce.mail.keynova.aiwithbehavior_on_mx_failure = REJECT_MESSAGE(no silent fallback to amazonses.com that would break SPF alignment).
Warm-up evidence
Initial sandbox warm-up (constrained by the 200 sends/24hr sandbox cap):
| Date | Sandbox sends | Bounce rate | Complaint rate |
|---|---|---|---|
| 2026-05-30 | 50 | 0% | 0% |
The warm-up will continue at the sandbox cap (~150–200/day) through to production-access approval. CloudWatch reputation metrics (Reputation.BounceRate, Reputation.ComplaintRate) attached.
Engineering hardening already in place
- SES configuration set with
reputation_metrics_enabled = true, TLS-required delivery, and SES-side suppression list (BOUNCE,COMPLAINT) as belt-and-suspenders. - SNS topic policy scoped: only
ses.amazonaws.comcan publish, and only events from this exact configuration set ARN. - IRSA-based credential delivery to the notification service (no static AWS keys anywhere in the codebase; the pod assumes role
keynova-prod-notification-sesvia the EKS OIDC provider). The role's IAM policy explicitly grantsses:SendEmail,ses:SendRawEmail,ses:PutSuppressedDestination,ses:GetSuppressedDestination,ses:DeleteSuppressedDestination, andses:ListSuppressedDestinations— scoped to the identity ARN where possible. - 41/41 Jest tests in the notification service cover the entire send/webhook/unsubscribe surface, including:
SesWebhookController— 14 cases (signature verification, topic-ARN allow-list, SubscriptionConfirmation handshake, hard/soft bounce branching, complaint, delivery, cross-tenant safety, idempotency, monotonic status, malformed JSON, text/plain body handling)EmailService— 25 cases (forced-From injection,ConfigurationSetNamepropagation,List-Unsubscribeheader presence, RFC 8058 one-click, per-recipient HMAC token in footer, fallback generic URL when no guest matches)CommPreferencesService— adds 3 cases forunsubscribeByEmail(SES suppression call, DB opt-out per matching guest row, silent success when no match)
Requested production-access changes
- Move account 943273444105 out of the SES sandbox in ap-south-1.
- Initial daily sending limit: 50,000/day (10× initial target, leaves headroom).
- Initial max send rate: 14/second (default cap is fine; we send <2/s peak).
We will operate well below these caps in v1 and only request increases if usage shows sustained growth above 80% of cap.