How to Receive Emails from Contact Form with AWS SES Lambda and API Gateway: Complete Guide
By Braincuber Team
Published on April 4, 2026
What You'll Learn:
- Why the traditional mailto approach is problematic for privacy and user experience
- How to set up AWS SES and verify email addresses for sending
- How to create an IAM role with SES permissions for Lambda functions
- How to write a Lambda function that receives form data and sends emails via SES
- How to configure API Gateway as an HTTP trigger for your Lambda function
- How to build a complete HTML form with JavaScript that submits to your AWS backend
Building a contact form that sends emails without exposing your email address is a common requirement for modern websites. The traditional approach of using a mailto: link has two major inconveniences: it forces both parties to share their emails with one another, and it forces the visitor to open their default mail program, which can be frustrating on public computers or for privacy-minded individuals. In this complete tutorial, we will build a serverless contact form using AWS SES, Lambda, and API Gateway. This step by step guide will show you exactly how to implement this functionality from scratch, giving you full control without relying on third-party widgets or paid subscriptions. Whether you are a beginner or experienced developer, this beginner guide will show you exactly how to build a practical cloud-powered feature.
Backend Architecture Overview
We will use three AWS services working together to create our serverless email pipeline. When our form is submitted, the following workflow will happen:
1. API Gateway
Receives the POST request from the browser with form data in the request body, validates it, and triggers the Lambda function.
2. Lambda Function
Extracts form data from the event body, builds the email content, and invokes SES to send the actual email message.
3. AWS SES
Simple Email Service turns the email data into an actual text email and sends it to the recipient via AWS mail servers.
Once the email is sent, our browser will receive a response with status code 200 and a success message. If any step in the AWS cloud fails, the response will have a 500 status code. The beauty of this architecture is that you do not have to worry about running your backend code on a server 24/7 and maintaining that server. AWS takes care of that under the hood so you can only focus on writing code. Additionally, you only get billed for the number of times your function gets called and the amount of time it takes to execute, and it is incredibly cheap.
Step 1: Set Up AWS SES
We are going to set up each one of these steps in the reverse order, beginning with SES, which is going to be easier. First in your AWS console, go to the SES service, then click on Email Addresses in the side menu, then click on the "Verify a New Email Address" button.
Verify Sender Email Address
In the dialogue that opens up, enter the email address that you want the SES service to put as the sender when it sends the email. This will send an email with a verification link to click.
Click Verification Link
Open the email you received from AWS and click the verification link. This is how AWS knows that the owner of the email consents to having their email address used as the sender address. Until you verify, the status will show as pending.
Verify Status Changed to Verified
Once verified, refresh the SES console page and the verification status should change to verified. You can optionally test the service by selecting your verified email and clicking "Send a Test Email".
Important: Region Matters
Make note of the AWS region where you registered and verified your email. You will need to use this exact same region in your Lambda function code. If you use a different region, SES will not be able to send emails.
Step 2: Create IAM Role and Policy for Lambda
Before we start writing our Lambda function, we need to create an IAM role to attach it to the function and grant it permissions (referred to as policies in AWS) to invoke the SES service.
Create the IAM Policy
From your AWS console, go to the IAM service, click on Policies in the side menu, then click on the "Create Policy" button. In the policy creation page, go to the JSON tab and paste the following permissions.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"ses:SendEmail",
"ses:SendRawEmail"
],
"Resource": "*"
}
]
}
Name the policy something memorable and click the "Create Policy" button.
Create the IAM Role
Now we create an IAM role which will be attached to the Lambda and link it to the permissions policy which we just created. From the IAM side menu, click Roles then click the "Create role" button.
Select Trusted Entity
Make sure the type selected is "AWS service" and select the Lambda case, then click "Next: Permissions".
Attach the SES Policy
Search for the policy we created earlier by its name and select it, then click next.
Name and Create Role
Give the role a name you can remember then click on "Create role".
Step 3: Create and Write the Lambda Function
Go to the Lambda service dashboard and click the "Create Function" button. In the function creation screen, name your function, select the "Author from scratch" option, and choose Node.js as the runtime.
Under "Change default execution role" choose the "Use an existing role" option then choose the name of the role you created in the previous step from the "Existing role" drop down. Finally, click the "Create function" button.
Initial Lambda Code
In the editor, open the index.js file (this is the file that will be executed when your Lambda is called), and replace its content with the following code:
const aws = require("aws-sdk");
const ses = new aws.SES({ region: "us-east-1" });
exports.handler = async function (event) {
console.log('EVENT: ', event)
const params = {
Destination: {
ToAddresses: ["your@email.com"],
},
Message: {
Body: {
Text: {
Data: "Hello from Lambda!"
},
},
Subject: { Data: "Message from AWS Lambda" },
},
Source: "your@email.com",
};
return ses.sendEmail(params).promise()
};
Notice that on line 2 we are using the AWS SDK and creating an SES instance. The reason I chose us-east-1 as the region is because that is where I registered and verified my email. Be sure to replace the email and use the AWS region where you registered your email.
Testing the Lambda Function
To test this function, click on the "Deploy" button. Then click on the Test button, then "Configure test event" which should open up a test configuration dialogue where you can create a new test event.
In the test event body editor, enter the following JSON which mimics what will eventually come from our browser request:
{
"body": {
"senderName": "Namo",
"senderEmail": "namo@trains.com",
"message": "I love trains!"
}
}
Now clicking the test button will run the test we just created. It should open a new tab in the editor to show us the logs created from running the function. The event object we logged out shows here under Function logs with the body data we used in the test event. This test should have sent an email to your inbox as well.
Final Lambda Code with Dynamic Data
Now let us modify our function code to get a more meaningful message from the test data. It is important to note that when API Gateway calls our function it will pass a string to the event body. This is why I use JSON.parse on event.body, to turn it into JSON and extract our sender's email, name, and message.
const aws = require("aws-sdk");
const ses = new aws.SES({ region: "us-east-1" });
exports.handler = async function (event) {
console.log('EVENT: ', event)
// Extract the properties from the event body
const { senderEmail, senderName, message } = JSON.parse(event.body)
const params = {
Destination: {
ToAddresses: ["your@email.com"],
},
// Interpolate the data in the strings to send
Message: {
Body: {
Text: {
Data: "You just got a message from " + senderName + " - " + senderEmail + ": " + message
},
},
Subject: { Data: "Message from " + senderName },
},
Source: "your@email.com",
};
return ses.sendEmail(params).promise();
};
Then I use those variables in the email body text and subject using string interpolation. If you try the test it, the code will return an error. This is because the test is passing a JSON object to event.body and we are using JSON.parse on JSON, which causes an error in JavaScript. Sadly, the test editor does not allow us to pass strings to the event, so we will have to test that later from the browser.
Step 4: Set Up API Gateway
Next, the last AWS service we are going to use is API Gateway, which will enable our browser to send HTTP requests to the Lambda function we created. Without leaving your Lambda function page, expand the "Function overview" section and click on "Add trigger".
Add API Gateway Trigger
Choose API Gateway from the dropdown, HTTP API as the API type, "Open" as the security mechanism, and check the CORS checkbox option. Then click "Add".
Copy the API Endpoint URL
You should be redirected to the "Configuration" tab of your function, showing you the new API Gateway trigger you just created. From there, note the API endpoint. This is the URL we are going to be calling from our browser with the form data.
Step 5: Build the HTML Form and JavaScript
Now we can finally test the form to see if it sends emails or not. Let us start with a basic HTML form with no CSS, just to test our desired functionality.
<h2>Contact Us</h2>
<form>
<label for="name">Name:</label>
<input name="name" type="text"/><br/><br/>
<label for="email">Email:</label>
<input name="email" type="email"/><br/><br/>
<label for="message">Message:</label>
<textarea name="message"></textarea><br/><br/>
<input type="submit"/>
<div>
<p id="result-text"></p>
</div>
</form>
Now we want to handle the submit functionality with JavaScript. Let us modify our JavaScript to handle sending the request when the form is submitted.
const form = document.querySelector("form");
form.addEventListener("submit", (event) => {
// prevent the form submit from refreshing the page
event.preventDefault();
const { name, email, message } = event.target;
// Use your API endpoint URL you copied from the previous step
const endpoint =
"https://YOUR_API_ID.execute-api.us-east-1.amazonaws.com/default/sendContactEmail";
// We use JSON.stringify here so the data can be sent as a string via HTTP
const body = JSON.stringify({
senderName: name.value,
senderEmail: email.value,
message: message.value
});
const requestOptions = {
method: "POST",
body
};
fetch(endpoint, requestOptions)
.then((response) => {
if (!response.ok) throw new Error("Error in fetch");
return response.json();
})
.then((response) => {
document.getElementById("result-text").innerText =
"Email sent successfully!";
})
.catch((error) => {
document.getElementById("result-text").innerText =
"An unknown error occurred.";
});
});
We use JSON.stringify here so the data can be sent as a string via HTTP. The event.preventDefault() prevents the form submit from refreshing the page. The fetch API sends a POST request to our API Gateway endpoint with the form data in the request body.
Step 6: Test the Complete Workflow
Now, the moment of truth: fill in the form and click submit. If you see the success message, that means the email was sent. Since you own the email the message was sent to, take a quick look at your inbox to see that you received an email with the details you used in the form.
| Step | What Happens | Expected Result |
|---|---|---|
| 1. Fill form | User enters name, email, and message | Form fields populated with user data |
| 2. Click Submit | JavaScript sends POST request to API Gateway | Request sent with JSON body containing form data |
| 3. API Gateway | Validates request and triggers Lambda function | Lambda receives event with form data in body |
| 4. Lambda Function | Parses event body, builds email params, calls SES | SES sendEmail invoked with correct parameters |
| 5. AWS SES | Sends email via AWS mail servers | Email delivered to recipient inbox |
| 6. Browser Response | Receives 200 status and success message | "Email sent successfully!" displayed to user |
Customization Options
Of course you can customize this flow in terms of using a framework on the frontend like React or Vue or a different programming language for the Lambda like Python or Go. Here are some common customizations you might want to make.
Add CSS Styling
Style the form to match your website design with custom CSS, form validation, and responsive layout.
Add Input Validation
Add client-side and server-side validation to ensure email format, required fields, and message length limits.
HTML Email Format
Use SES's HTML body option instead of Text to send beautifully formatted emails with your branding.
Multiple Recipients
Add multiple email addresses to the ToAddresses array or use CcAddresses and BccAddresses for routing.
SES Sandbox Limitation
By default, SES accounts start in the sandbox, which means you can only send emails to verified email addresses. To send to any email address, you need to request production access through the SES console. This is a common gotcha that trips up many developers.
AWS Services Used Summary
| Service | Full Name | Role in This Solution |
|---|---|---|
| SES | Simple Email Service | Serverless messaging service that sends the actual email messages via AWS mail servers |
| Lambda | AWS Lambda | Serverless compute that receives form data, builds email content, and invokes SES |
| API Gateway | Amazon API Gateway | HTTP API endpoint that receives browser POST requests and triggers the Lambda function |
| IAM | Identity and Access Management | Provides the Lambda function with permissions to invoke SES sendEmail and sendRawEmail |
Frequently Asked Questions
Why use a serverless contact form instead of mailto?
A mailto link exposes your email address to spammers and forces users to open their default mail program. A serverless form keeps your email private and lets users submit messages directly from the browser without leaving your website.
How much does this serverless contact form cost?
It is incredibly cheap. Lambda charges per invocation and execution time (first 1 million requests free/month). SES charges per email sent (first 62,000 emails free/month from EC2). API Gateway charges per API call (first 1 million HTTP API calls free/month).
Why does the Lambda function use JSON.parse on event.body?
When API Gateway calls the Lambda function, it passes the request body as a string, not a JSON object. JSON.parse converts this string back into a JavaScript object so we can extract the senderName, senderEmail, and message properties.
What is the SES sandbox and how do I exit it?
New SES accounts start in sandbox mode, which only allows sending to verified email addresses. To send to any email, go to the SES console, navigate to Account Dashboard, and request production access. AWS typically approves within 24 hours.
Can I use Python instead of Node.js for the Lambda function?
Yes. You can use Python, Go, Java, or any supported Lambda runtime. The logic remains the same: parse the event body, build SES parameters, and call sendEmail. Just use the appropriate AWS SDK for your chosen language.
Need Help with AWS Cloud Solutions?
Our experts can help you design serverless architectures, build cloud-native applications, and optimize your AWS infrastructure.
