How to Setup a Serverless Contact Form with AWS Lambda, reCAPTCHA and Storyblok


In this tutorial I’ll explain you how to setup an api endpoint that accepts a JSON to send an email and create an entry in Storyblok’s data storage. You can use this for a contact form or a comment form on your dynamic or static website.

The Lambda function also validates the request with Google reCAPTCHA to prevent spam.

Architectural overview

Let’s see how this works:

  1. The user sends the request together with the captcha parameter ‘g-recaptcha-response’ to the AWS API Gateway
  2. g-recaptcha-response will be validated by reCAPTCHA in the Lambda function
  3. If the validation was successful we’ll send and email via AWS SES
  4. Then we will create a content entry with the content type ‘form_entry’ in a Storyblok folder

AWS Lambda serverless form


To make things easy I created a Cloudformation template which you can use to launch the api with a single click. You can find the code of the template at following repository:

Clicking on the button above will bring you to the AWS Cloudformation setup page with a preselected template.

New cloudformation stack

The next screen let’s you define the configuration of the Lambda function. Fill out all the parameters:

  • StoryblokOauthToken

    You can generate a Storyblok OAuth token in the “My account” section. The OAuth token let’s the Lambda function access the management API of Storyblok to create content entries.

  • StoryblokParentFolderId

    The parent folder id tells the script in which Storyblok folder to create the content entries. When creating the folder define form_entry as default content type and click the checkbox of disabling the visual composer.

    Form entry

    After creating the folder click on it to get the folder id from the url: /me/spaces/39837/stories/index/THIS_IS_THE_FOLDER_ID

  • StoryblokSpaceId

    Get the space id from the url in Storyblok: /me/spaces/THIS_IS_THE_SACE_ID/stories

  • ToEmailAddress

    Define the email address where the notifications will be sent.

  • ReCaptchaSecret

    Define the reCAPTCHA secret from If you have not created a token yet create one with “Invisible reCAPTCHA” selected.

Cloudformation settings

On the next pages of the Cloudformation stack creation process you can continue with the default values and finally initiate the creation.

Creating the form

After creating the Cloudformation stack we’ll setup a simple contact form to test the API.

The HTML part

Paste following code somewhere on your page and replace YOUR_RECAPTCHA_PUBLIC_KEY with the “Site key” from Google reCAPTCHA.

<form id="form">
  <p class="form__errors" style="display: none;">Please fill in all fields.</p>

  <label for="name_input">Name</label><br>
  <input type="text" id="name_input" name="name"><br>
  <label for="message_input">Message</label><br>
  <textarea type="textarea" id="message_input" name="message"></textarea>

  <p class="form__success" style="display: none;">Sent successfully!</p>
  <p class="form__sending" style="display: none;">Sending...</p>

  <button type="submit" class="g-recaptcha form__button"

The Javascript part

Copy the following Javascript code after the contact form HTML.

<script src='' async defer></script>
  var handleFormSubmit = function () {
    var formApiEndpoint = 'YOUR_API_ENDPOINT'
    var successEl = document.querySelector('.form__success')
    var sendingEl = document.querySelector('.form__sending')
    var errorsEl = document.querySelector('.form__errors')
    var buttonEl = document.querySelector('.form__button')
    var nameInput = document.querySelector('#name_input')
    var messageInput = document.querySelector('#message_input')
    var recaptchaResponse = document.querySelector('#form textarea[name=\'g-recaptcha-response\']') = 'none' = 'none'

    if (nameInput.value == '' || messageInput == '') { = 'block' = 'block' = 'none'
    } else {
      var formRequest = new Request(formApiEndpoint, {
        method: 'POST',
        body: JSON.stringify({
          name: nameInput.value,
          message: messageInput.value,
          'g-recaptcha-response': recaptchaResponse.value

        .then(function(response) {
          if (response.status === 200) {
            return response.json()
          } else {
            throw new Error('Something went wrong on api server!')
        .then(function(response) {
 = 'block'
 = 'block'
 = 'none'
          nameInput.value = ''
          messageInput.value = ''
        }).catch(function(error) {
 = 'block'
 = 'block'
 = 'none'

  document.querySelector('.form__button').addEventListener('click', function() {
    document.querySelector('.form__sending').style.display = 'block'
    document.querySelector('.form__button').style.display = 'none'

Replace YOUR_API_ENDPOINT with your Amazon API-Gateway url which you can grab at the Outputs section of your Cloudformation stack:

Cloudformation output

Testing the API

Make sure that your domain and email is verified in the Amazon SES (Simple Email Service) management console. Otherwise sending will not work.

If you have setup everything correctly you should receive an email when sending the contact form. Additionally there should be created a content item in the predefined folder in Storyblok.


With this setup you can easily capture any user input and have the data stored in Storyblok for contact forms or a comments system. Thanks to Lambda you don’t need to run a server just for handling a few form submissions and use a static site generator for your website.

Github repository of this
Amazon AWS
Storyblok AppStoryblok
Alexander Feiglstorfer


Alexander Feiglstorfer

Passionate developer and always in search of the most effective way to resolve a problem. After working 13 years for agencies and SaaS companies using almost every CMS out there he founded Storyblok to solve the problem of being forced to a technology, proprietary template languages and the plugin hell of monolithic systems.