Configure a Form with PayloadRelay as the Backend
Send contact, feedback, or signup form submissions to email, Slack, or another target.
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
POSTwith theFormorJSONpayload format. - If you use the JavaScript
fetch()function, add the exact site origin to the endpoint CORS configuration.
Procedure#
1. Create the endpoint#
- Open
Endpointsand selectCreate endpoint. - Set the accepted method to
POST. - Set the payload format to
Form, or toJSONif you submit with JavaScript. - If you use the JavaScript
fetch()function, add the exact site origin toAllowed CORS originsinSecurity, for example,https://example.com. A native formPOSTdoes not need CORS. - Set a low
Max requests per minutevalue inDetails. - In
Outputs, attach a confirmed email target. - Save and copy the endpoint URL.
2. Basic HTML form (form-encoded)#
<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():
<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.
- Open the endpoint in
Endpoints. - In
Security, findAllowed CORS origins. - Add each domain that hosts your form:
https://example.comhttps://www.example.comhttp://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:
- In
Human verificationinSecurity, enable Cloudflare Turnstile or Google reCAPTCHA. Enter the provider secret. - Add the provider browser widget to the form. Submit its token with the configured field name, which is
cf-turnstile-responseorg-recaptcha-responseby default. KeepInclude captcha field in payloaddisabled, unless a destination needs the token. - Set
Max requests per minuteinDetailsto limit an automated flood. Start with a low value. Increase it after you see the normal traffic. - In the
Filtertab, add the field validation for the necessary fields, the types, the lengths, and the formats. A browserrequiredattribute helps the user, but a client can bypass it. - Monitor the
CAPTCHA_FAILED,RATE_LIMITED_ENDPOINT, andFIELD_VALIDATION_FAILEDoutcomes 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#
<!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:
- Open
Relay targetsand selectAdd target. - Select
Emailand enter the address that receives the submissions. - Use the confirmation link to complete the email target confirmation.
- 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 activityasCompleted(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 acceptsPOST.PAYLOAD_TOO_LARGE: keep the form payload in the plan limit.- No email: make sure that the email target is
Confirmedand that it is attached as an endpoint destination. FIELD_VALIDATION_FAILED: make sure that the payload format agrees with the endpoint format,FormorJSON.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.