FP FormPing Documentation
Open Dashboard

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');
i

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.