03 — Embedding · Programmatic
Bring your own form HTML.
When the script-tag widget and Takeover both feel too magical — for example because you're building a custom multi-step UI in React, Vue, or Svelte — drop down to the programmatic API. You write the form, you handle the events, and FormPing only takes care of the network call.
Loading the widget
Drop the script once at the top of your app — without any data attributes, so it does not auto-init.
<script src="https://widget.formping.ch/widget.js" defer></script>
Once loaded, the global window.FormPing object is available with two
methods: init(token, options) and send(data).
1. Initialise once
window.FormPing.init('YOUR_TOKEN_HERE', {
lang: 'en', // optional — 'de' | 'en' | 'fr' | 'es' | 'it' | 'pt'
theme: 'light', // optional — 'light' | 'dark'
});
Call init exactly once when your app boots. It stores the token and
starts the bot-protection clock — submissions that arrive too quickly after init are
rejected, so do not call it inside your submit handler.
2. Send a submission
Build whatever form HTML you like, then on submit pass the field values to
FormPing.send(). It returns a Promise resolving to a result object.
async function handleSubmit(event) {
event.preventDefault();
const formData = new FormData(event.target);
try {
const result = await window.FormPing.send({
name: formData.get('name'),
email: formData.get('email'),
message: formData.get('message'),
});
console.log('submitted', result);
// show your own success UI
} catch (err) {
console.error('submit failed', err);
// show your own error UI
}
}
React example
import { useEffect, useState } from 'react';
const TOKEN = import.meta.env.VITE_FORMPING_TOKEN;
export function ContactForm() {
const [sent, setSent] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (window.FormPing) window.FormPing.init(TOKEN, { lang: 'en' });
}, []);
async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setError(null);
const fd = new FormData(e.currentTarget);
try {
await window.FormPing.send({
name: String(fd.get('name') ?? ''),
email: String(fd.get('email') ?? ''),
message: String(fd.get('message') ?? ''),
});
setSent(true);
} catch (err) {
setError((err as Error).message);
}
}
if (sent) return <p>Thanks — we will get back to you soon.</p>;
return (
<form onSubmit={onSubmit}>
<input name="name" placeholder="Name" required />
<input name="email" type="email" placeholder="Email" required />
<textarea name="message" placeholder="Message" required />
<button type="submit">Send</button>
{error && <p role="alert">{error}</p>}
</form>
);
}
Waiting for the script to load
If you load the widget script lazily (after your app code runs), wait for the global
before calling init:
function waitForFormPing() {
return new Promise((resolve) => {
if (window.FormPing) return resolve();
const i = setInterval(() => {
if (window.FormPing) { clearInterval(i); resolve(); }
}, 50);
});
}
await waitForFormPing();
window.FormPing.init('YOUR_TOKEN_HERE');
Programmatic mode gives you total control over markup, validation, and styling — but you also lose the form-builder live preview. If you only need a custom look, theming + Custom CSS on a builder form is usually less work.