# Add a contact form to an Amazon S3 static website

> S3 website endpoints accept only GET and HEAD over plain HTTP, so post the contact form to a form backend and get redirects right with CloudFront.

Published 2026-10-25 · https://ikiform.com/guides/contact-form-static-site/aws-s3

An S3 website endpoint answers a form `POST` with `405 MethodNotAllowed`, so point the form's `action` at your Ikiform endpoint instead. Because the endpoint is plain `http:`, a `_redirect` back to it is never followed; set **Redirect after submit** in the form's settings, or put CloudFront in front for `https:`. With CloudFront in front of the bucket's REST endpoint, redirect to a full file name such as `thanks.html`, because the default root object applies only to the root.

## What's different on S3

A bucket configured for static website hosting gets its own website endpoint, such as `http://www.example.com.s3-website-us-east-1.amazonaws.com`. AWS documents that it supports only `GET` and `HEAD` requests on objects. A form post gets an HTML error page:

`Response to a POST`:

```html
<html>
<head><title>405 Method Not Allowed</title></head>
<body>
<h1>405 Method Not Allowed</h1>
<ul>
<li>Code: MethodNotAllowed</li>
<li>Message: The specified method is not allowed against this resource.</li>
<li>Method: POST</li>
<li>ResourceType: OBJECT</li>
```

Three more S3 traits shape the contact form:

| S3 trait | What it means for the form |
| --- | --- |
| Website endpoints don't support HTTPS | Ikiform only follows a `_redirect` to an `https:` URL, so a redirect back to the bare endpoint is ignored |
| Response headers come from CloudFront, not the bucket | A Content-Security-Policy is set in a CloudFront response headers policy |
| CloudFront's default root object only applies to the root | With a REST origin, `https://example.com/thanks/` doesn't load `thanks/index.html`; redirect to `thanks.html` |

For an `https:` site, AWS recommends Amplify Hosting, which runs on CloudFront, or CloudFront in front of the bucket. The form works either way. What changes is where the visitor ends up after sending.

Diagram: Where the visitor lands after sending, with and without CloudFront

1. On the bare website endpoint, the page is served over http and posts to Ikiform.
2. Ikiform stores the message, but ignores a _redirect to an http address and shows its hosted thank-you page.
3. Setting Redirect after submit in the dashboard is the way around that, because it accepts http URLs.
4. Behind CloudFront with your own domain, the page is https.
5. Ikiform follows the _redirect to your thanks.html, as long as the domain is in Allowed domains.

The message is stored either way. Only the last step, where the browser goes, depends on https.

## Which CloudFront origin

CloudFront can read from the bucket in two ways, and the choice decides how your thank-you URL must look:

| Origin | Bucket | Folder index pages | Thank-you URL |
| --- | --- | --- | --- |
| The S3 website endpoint, as a custom origin | Must be publicly readable | Work: `/thanks/` loads `thanks/index.html` | `/thanks/` or `/thanks.html` |
| The REST endpoint, with origin access control | Stays private, read only by CloudFront | Only the root, through the default root object | `/thanks.html`, a full file name |

A full file name works with both, which is why the samples use one. If you'd rather not run CloudFront yourself, Amplify Hosting can deploy from the bucket with `https:` and a managed CDN; the form needs no change there either.

## The form

S3 sites are often company sites, so this form asks for a work email and the company name. Upload it as `contact.html` next to `thanks.html`:

`contact.html`:

```html
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
  <label for="name">Name</label>
  <input id="name" name="name" autocomplete="name" required />

  <label for="email">Work email</label>
  <input id="email" name="email" type="email" autocomplete="email" required />

  <label for="company">Company</label>
  <input id="company" name="company" autocomplete="organization" />

  <label for="message">How can we help?</label>
  <textarea id="message" name="message" rows="6" minlength="10" required></textarea>

  <input type="hidden" name="_redirect" value="https://www.example.com/thanks.html" />
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
  <button type="submit">Send</button>
</form>
```

- The `_redirect` points at the CloudFront domain, with a file name. It's followed only when `www.example.com` or `example.com` is under **Allowed domains**.
- On the bare `http:` endpoint, remove the `_redirect` input and set **Redirect after submit** to the thank-you page instead. That setting accepts `http:` URLs, because only your team can change it.
- Keep `email` as the field name. Notification emails use it as the reply-to address.

On a company site, more than one person usually reads the contact inbox. Add up to 10 confirmed notification recipients to the form; each gets the message, and whoever presses **Reply** answers the sender directly. If messages should also reach a shared channel or a ticket queue, add a signed webhook and route it through [Zapier](https://ikiform.com/blog/form-submissions-to-zapier.md) or your own code. The `company` field arrives as plain text, so it's ready to filter on in either place.

Before you upload, test the page from a local server with `localhost` under **Allowed domains**. Posts from `localhost` count as tests, so you can get the form right before the bucket ever serves it.

Sync the folder to the bucket, and invalidate the pages in CloudFront so the new form is served right away:

`Terminal`:

```bash
aws s3 sync ./site s3://www.example.com --delete

aws cloudfront create-invalidation --distribution-id E1EXAMPLE --paths "/contact.html" "/thanks.html"
```

## Allowed domains on AWS hosts

| Where the page is served | Add |
| --- | --- |
| Bare website endpoint | The bucket's full endpoint host, such as `www.example.com.s3-website-us-east-1.amazonaws.com` |
| CloudFront default domain | The exact `d111111abcdef8.cloudfront.net` host, while you test |
| Your own domain | `example.com`, which also covers `www.example.com` |

Never add `amazonaws.com`, a regional `s3-website` suffix or `cloudfront.net` on their own. Every bucket or distribution in the world would match. AWS registers the S3 website endpoint domains in the Public Suffix List for the same reason: each bucket is a separate site.

## Headers through CloudFront

S3 object metadata can't set a Content-Security-Policy, so set it in a CloudFront response headers policy and attach it to the distribution's default cache behavior. The header value is limited to 1,783 characters:

`csp-policy.json`:

```json
{
  "Name": "site-csp",
  "SecurityHeadersConfig": {
    "ContentSecurityPolicy": {
      "Override": true,
      "ContentSecurityPolicy": "default-src 'self'; form-action 'self' https://ikiform.com; connect-src 'self' https://ikiform.com"
    }
  }
}
```

`Terminal`:

```bash
aws cloudfront create-response-headers-policy --response-headers-policy-config file://csp-policy.json
```

> `Override: true` replaces any CSP the origin sends. With a bare S3 origin there isn't one, but it keeps the policy in charge if you change origins later.

## Check it works

1. Add the domains from the table above, plus `localhost` while you test.
2. Submit from your CloudFront domain. You should land on `thanks.html` on your own domain.
3. If you also keep the bare website endpoint public, submit from it and confirm you reach the page set in **Redirect after submit**.
4. Open **Submissions** in the dashboard. The new submission is there with every field you sent.
5. Check the inbox of each notification recipient. Each gets an email with the fields, and **Reply** goes to the submitter when the form has an `email` field.
6. Nothing arrived? Switch the dashboard's status filter to **Spam**. Spam gets the same success answer as a real submission.

## Gotchas

| Problem | What to do |
| --- | --- |
| `405 Method Not Allowed` with `Code: MethodNotAllowed` | The form posted to the bucket. CloudFront may still serve the old `contact.html`; invalidate it. |
| `AccessDenied` or `403` after the redirect | The redirect points at a folder such as `/thanks/` on a CloudFront distribution with a REST origin. Point it at `thanks.html`. |
| The browser lands on Ikiform's thank-you page instead of yours | `_redirect` is ignored unless its domain is in **Allowed domains** and it is an `https:` URL. Add the domain, or set **Redirect after submit** in the form's settings instead. |
| `403 origin_not_allowed`, or every post lands in spam | The page's domain isn't in the form's **Allowed domains**. Add it, plus `localhost` for local tests. Subdomains of a listed domain are allowed. |

## Questions

### Can an S3 static website handle form submissions?

No. S3 website endpoints support only GET and HEAD requests, so a POST gets 405 MethodNotAllowed. Point the form at a form backend, or build an API with Lambda.

### Do I need CloudFront for the contact form to work?

No. The form posts to Ikiform from an http page too. CloudFront gives you https, which lets Ikiform follow a _redirect back to your own thank-you page.

### Why not API Gateway, Lambda and SES?

That works and keeps everything in AWS, but you build the validation, spam filtering, storage and retries yourself, and SES needs a verified sender. A form backend is less to maintain for a contact form.

### Will CloudFront cache the old form after I change it?

It can, until the cached copy expires. Invalidate contact.html after each change, or upload HTML with a short Cache-Control max-age so new forms go live quickly.

### Is Ikiform free?

No, there is no free plan. Testing is free: you can send 50 test submissions a month from localhost before you pay. To go live, Pro starts at $6/mo for 500 submissions, or you can pay once for 10,000 submissions for $49 or 50,000 submissions for $149, with credits that never expire.

## More guides

- [Contact form on a static site](https://ikiform.com/guides/contact-form-static-site.md): Add a working contact form to a static site with no server: where the POST goes, spam, replies by email, and code for every host and framework.
- [Cloudflare Pages](https://ikiform.com/guides/contact-form-static-site/cloudflare-pages.md): Add a contact form to Cloudflare Pages without writing a Function: post to a form backend, set headers in _headers and use your own Turnstile keys.
- [GitHub Pages](https://ikiform.com/guides/contact-form-static-site/github-pages.md): GitHub Pages can't run server code, so a form POST gets 405 Not Allowed; post to a form backend instead and get project-site redirects right.
- [Vercel](https://ikiform.com/guides/contact-form-static-site/vercel.md): A static page on Vercel answers a form POST with 405 Method Not Allowed; here is why, and how to send the form to a backend instead.
- [Netlify](https://ikiform.com/guides/contact-form-static-site/netlify.md): Run a contact form on a Netlify site without Netlify Forms: no build-time detection, JSON posts from React, and a separate form for deploy previews.
- [Portfolio website](https://ikiform.com/guides/contact-form-static-site/portfolio-website.md): A portfolio contact form that asks the questions you'd ask on a first call: project, budget, timing and a link, with no server and replies from your inbox.
