← Back to documentation

Configure a Form with PayloadRelay as the Backend

Send contact, feedback, or signup form submissions to email, Slack, or another target.

4 min read

Use PayloadRelay as the backend for an HTML form. PayloadRelay sends a submission to your configured targets, such as email, Slack, or a webhook. You do not need server-side code.

Purpose#

Use this example to:

  • Build an HTML form that posts directly to a PayloadRelay endpoint.
  • Send form data or JSON.
  • Configure an email target for the form entries.
  • Configure CORS for a JavaScript submission.
  • Add abuse controls for a public form.
  • Add a style to the form, and add browser validation.

Before you start#

  • Create a PayloadRelay endpoint.
  • Attach one confirmed relay target as a minimum. Email is a good destination for a contact form.
  • Configure the endpoint to accept POST with the Form or JSON payload format.
  • If you use the JavaScript fetch() function, add the exact site origin to the endpoint CORS configuration.

Procedure#

1. Create the endpoint#

  1. Open Endpoints and select Create endpoint.
  2. Set the accepted method to POST.
  3. Set the payload format to Form, or to JSON if you submit with JavaScript.
  4. If you use the JavaScript fetch() function, add the exact site origin to Allowed CORS origins in Security, for example, https://example.com. A native form POST does not need CORS.
  5. Set a low Max requests per minute value in Details.
  6. In Outputs, attach a confirmed email target.
  7. Save and copy the endpoint URL.

2. Basic HTML form (form-encoded)#

Code Example
<form
  action="https://api.payloadrelay.com/relay/YOUR_ENDPOINT_ID"
  method="POST"
>
  <label for="name">Name</label>
  <input type="text" id="name" name="name" required />

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

  <label for="message">Message</label>
  <textarea id="message" name="message" rows="5" required></textarea>

  <button type="submit">Send</button>
</form>

When a user submits the form, the browser sends an application/x-www-form-urlencoded POST. PayloadRelay parses the fields and sends them to the email target.

This form shows the basic submission. Before you publish it on an open website, add the public-form protections in step 5.

3. JSON submission with JavaScript#

If you must have a loading state, error handling, or a submission with no page navigation, use fetch():

Code Example
<form id="contact-form">
  <label for="name">Name</label>
  <input type="text" id="name" name="name" required />

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

  <label for="message">Message</label>
  <textarea id="message" name="message" rows="5" required></textarea>

  <button type="submit">Send</button>
  <p id="status"></p>
</form>

<script>
  const form = document.getElementById("contact-form");
  const status = document.getElementById("status");

  form.addEventListener("submit", async (e) => {
    e.preventDefault();
    status.textContent = "Sending…";

    const data = Object.fromEntries(new FormData(form));

    try {
      const res = await fetch(
        "https://api.payloadrelay.com/relay/YOUR_ENDPOINT_ID",
        {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(data),
        }
      );

      if (res.ok) {
        status.textContent = "Sent! We'll be in touch.";
        form.reset();
      } else {
        status.textContent = "Something went wrong. Please try again.";
      }
    } catch {
      status.textContent = "Network error. Please try again.";
    }
  });
</script>

If you use a JSON submission, set the endpoint payload format to JSON.

4. Configure CORS#

Cross-origin resource sharing (CORS) lets JavaScript read a response from a different origin. PayloadRelay lets you set the CORS origins for each endpoint.

  1. Open the endpoint in Endpoints.
  2. In Security, find Allowed CORS origins.
  3. Add each domain that hosts your form:
    • https://example.com
    • https://www.example.com
    • http://localhost:3000 (for local development)

If you do not configure CORS, a browser fetch() call fails with a CORS error. A native <form> submission does not need CORS, but it moves the browser away from the page.

CORS controls which browser pages can read a response. It is not authentication. A script, a bot, and a server-side client can call a public endpoint with any CORS configuration.

5. Protect a public form#

Before you publish a form endpoint:

  1. In Human verification in Security, enable Cloudflare Turnstile or Google reCAPTCHA. Enter the provider secret.
  2. Add the provider browser widget to the form. Submit its token with the configured field name, which is cf-turnstile-response or g-recaptcha-response by default. Keep Include captcha field in payload disabled, unless a destination needs the token.
  3. Set Max requests per minute in Details to limit an automated flood. Start with a low value. Increase it after you see the normal traffic.
  4. In the Filter tab, add the field validation for the necessary fields, the types, the lengths, and the formats. A browser required attribute helps the user, but a client can bypass it.
  5. Monitor the CAPTCHA_FAILED, RATE_LIMITED_ENDPOINT, and FIELD_VALIDATION_FAILED outcomes in Request activity.

Do not put a Bearer token, an API key, or a different reusable secret in public HTML or JavaScript. A visitor can read and reuse a credential that goes to the browser.

6. Styled contact form example#

Code Example
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Contact Us</title>
    <style>
      * { box-sizing: border-box; margin: 0; padding: 0; }
      body { font-family: system-ui, sans-serif; background: #f5f5f5; padding: 2rem; }
      .form-card {
        max-width: 480px; margin: 0 auto; background: #fff;
        border-radius: 8px; padding: 2rem; box-shadow: 0 2px 8px rgba(0,0,0,0.1);
      }
      h1 { font-size: 1.5rem; margin-bottom: 1.5rem; }
      label { display: block; font-weight: 600; margin-bottom: 0.25rem; font-size: 0.9rem; }
      input, textarea {
        width: 100%; padding: 0.5rem; border: 1px solid #ccc;
        border-radius: 4px; margin-bottom: 1rem; font-size: 1rem;
      }
      textarea { resize: vertical; }
      button {
        background: #2563eb; color: #fff; border: none; padding: 0.75rem 1.5rem;
        border-radius: 4px; font-size: 1rem; cursor: pointer; width: 100%;
      }
      button:hover { background: #1d4ed8; }
      #status { margin-top: 1rem; text-align: center; font-size: 0.9rem; }
    </style>
  </head>
  <body>
    <div class="form-card">
      <h1>Contact Us</h1>
      <form id="contact-form">
        <label for="name">Name</label>
        <input type="text" id="name" name="name" required />

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

        <label for="subject">Subject</label>
        <input type="text" id="subject" name="subject" />

        <label for="message">Message</label>
        <textarea id="message" name="message" rows="5" required></textarea>

        <button type="submit">Send Message</button>
        <p id="status"></p>
      </form>
    </div>

    <script>
      const form = document.getElementById("contact-form");
      const status = document.getElementById("status");
      const btn = form.querySelector("button");

      form.addEventListener("submit", async (e) => {
        e.preventDefault();
        btn.disabled = true;
        status.textContent = "Sending…";

        const data = Object.fromEntries(new FormData(form));

        try {
          const res = await fetch(
            "https://api.payloadrelay.com/relay/YOUR_ENDPOINT_ID",
            {
              method: "POST",
              headers: { "Content-Type": "application/json" },
              body: JSON.stringify(data),
            }
          );

          if (res.ok) {
            status.textContent = "✓ Message sent successfully!";
            status.style.color = "#16a34a";
            form.reset();
          } else {
            status.textContent = "⚠ Failed to send. Please try again.";
            status.style.color = "#dc2626";
          }
        } catch {
          status.textContent = "⚠ Network error. Please try again.";
          status.style.color = "#dc2626";
        } finally {
          btn.disabled = false;
        }
      });
    </script>
  </body>
</html>

7. Configure the email target#

When an email target receives a form submission, PayloadRelay formats the fields in the email body. To configure this behavior:

  1. Open Relay targets and select Add target.
  2. Select Email and enter the address that receives the submissions.
  3. Use the confirmation link to complete the email target confirmation.
  4. Attach the target to your form endpoint in Outputs.

Each form submission makes an email that contains the submitted field names and values.

Expected result#

  • A form submission appears in Request activity as Completed (ACCEPTED).
  • The email target receives the formatted field data in seconds.
  • The browser console shows no CORS error.
  • The form resets and shows a success message after the submission.
  • A public deployment rejects an invalid captcha token, and it applies the configured endpoint rate rules and field-validation rules.

Common issues#

  • A CORS error in the browser console: add the exact origin, with the protocol and the port, to the endpoint Allowed CORS origins.
  • METHOD_NOT_ALLOWED: make sure that the endpoint accepts POST.
  • PAYLOAD_TOO_LARGE: keep the form payload in the plan limit.
  • No email: make sure that the email target is Confirmed and that it is attached as an endpoint destination.
  • FIELD_VALIDATION_FAILED: make sure that the payload format agrees with the endpoint format, Form or JSON.
  • CAPTCHA_FAILED: make sure that the browser widget token field agrees with the configured captcha field name. Make sure that the provider secret is current.