JavaScript
fetchA complete browser integration: send JSON, keep the visitor on the page, and handle the three outcomes that actually happen.
Send a submission
Send JSON and read error.message when the request fails. That message is written to be shown to a person.
JavaScript
const ENDPOINT = "https://formivo.faizyab.me/api/f/YOUR_FORM_ID";
async function submitForm(values) {
const response = await fetch(ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/json",
// Makes a retry safe: the same key never creates a second submission.
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify(values),
});
const result = await response.json().catch(() => null);
if (!response.ok || !result?.success) {
throw new Error(result?.error?.message ?? "The form could not be sent.");
}
return result; // { success: true, leadId, message }
}Wire it to a form
Prevent the default submit, disable the button while the request is in flight, and keep the entered values when something goes wrong so the visitor does not retype them.
JavaScript
const form = document.querySelector("#contact-form");
form.addEventListener("submit", async (event) => {
event.preventDefault();
const button = form.querySelector("button[type=submit]");
const status = form.querySelector("[data-status]");
button.disabled = true;
status.textContent = "Sending…";
try {
const values = Object.fromEntries(new FormData(form).entries());
// Keep the visitor on the page if the request fails; the values stay in the inputs.
const result = await submitForm(values);
form.reset();
status.textContent = "Thanks, we received your message.";
console.info("Stored submission", result.leadId);
} catch (error) {
status.textContent = error.message;
} finally {
button.disabled = false;
}
});Retry without duplicates
Generate the idempotency key once per logical submission, not per attempt. Retry 429 and 5xx, and never retry a 4xx client error.
JavaScript
async function submitWithRetry(values, attempts = 3) {
const idempotencyKey = crypto.randomUUID();
for (let attempt = 1; attempt <= attempts; attempt += 1) {
try {
const response = await fetch(ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify(values),
});
// A client error will fail again: surface it instead of retrying.
if (response.status >= 400 && response.status < 500) {
const body = await response.json().catch(() => null);
throw new Error(body?.error?.message ?? "The form could not be sent.");
}
if (response.ok) return response.json();
const retryAfter = Number(response.headers.get("Retry-After") ?? "1");
await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000 * attempt));
} catch (error) {
if (attempt === attempts) throw error;
await new Promise((resolve) => setTimeout(resolve, 400 * attempt));
}
}
throw new Error("The form could not be sent.");
}Server-side (Node) is the same call
Node 18 and later have fetch built in, so the first example works unchanged in a server route, a script or an edge function.