form.js

Less typing.More to work with.

Add autocomplete, checks, and useful data to the form you already have.

Try form.js

Add to your form

Stay in the loop.

Sent with your form.

Your fields + hidden inputs. One submission to your backend.

Hidden input 19 fields
{
  "email": "",
  "email_domain": "",
  "email_domain_valid": "",
  "email_disposable": "",
  "email_role": "",
  "ip": "",
  "ip_country": "",
  "ip_asn": "",
  "ip_state": "",
  "ip_city": "",
  "ip_vpn": "",
  "ip_tor": "",
  "ip_relay": "",
  "ip_datacenter": "",
  "device": "",
  "os": "",
  "browser": "",
  "bot": "",
  "bot_name": ""
}

Loading lookups...

Add form.js

Set up the newsletter example

Register your website. Get your code. Try it on your form.

A small start · HTML
<script src="https://cdn.parseapi.com/v1/form.js"
        data-key="parse_public_YOUR_KEY" defer></script>

<input name="full_name" data-parse="name">
<input type="hidden" name="first_name" data-parse-fill="first">
<input type="hidden" name="last_name" data-parse-fill="last">

Already have a public key? Replace parse_public_YOUR_KEY and paste this inside your form.

API keys ↗

Speak their language.

Set your form's language. Built-in checks and suggestions follow it.

<form lang="es">
  <input type="email" name="email" data-parse>
</form>

Include form.js once. Your labels and submit handler stay yours. Demo submissions stay on this page.

Sigamos en contacto

Language settings

Built-in feedback supports English, Spanish, French, German, Brazilian Portuguese and Japanese. Set lang on your page, form or field. Use data-parse-lang for a ParseAPI-only override. Without either, the browser language is used. Unsupported languages fall back to English.

Call parseform.scan() after changing language to update existing feedback. Your field values, API answers, country settings and request count stay the same. Translate your own labels and buttons in your application.

Less waiting.
By design.

Checks run after a pause in typing. Answers fill your fields as they arrive. Unfinished lookups never hold up submission.

From your browser

Five requests to /ip. Full response time, including the body and any connection setup.

Field lookups include a typing pause and may take longer. Worldwide speed and status

Your form.
Your rules.

Keep your design, validation, and backend.

You choose the fields.

Enhance the inputs you choose. Visit details are optional.

Unknown stays unknown.

Missing answers stay empty. You choose what is required.

Your form keeps working.

Failed lookups do not block submission. Your validation still applies.

Marked values go to ParseAPI. Submissions go to your backend. Privacy Policy · Security.

Know a little more.

Add location, connection, and browser details to your form.

Location
IP address, approximate country, state, and city.
Connection
ASN, VPN, Tor, Private Relay, and hosting flags.
Browser
Device, OS, browser, and recognized bots.

Core details work on Free. Deep data is included with paid plans.

Location is approximate and browser details can be changed. A VPN or country mismatch alone does not prove fraud.

Use context on your server

Hidden fields can be edited. Before enforcing a rule, look up the visitor IP observed by your server with your secret API key. Use your framework's trusted proxy configuration; do not take the IP from the submitted form or blindly trust a forwarded header. The request's User-Agent is also client-controlled.

This Node.js helper returns the same visit fields. Pass the request-derived IP and User-Agent. Missing answers are null; false means not detected. Your backend decides what needs review.

form-context-server.node.mjs
import { isIP } from 'node:net';

const text = value => typeof value === 'string' && value.trim() ? value : null;
const flag = value => typeof value === 'boolean' ? value : null;
const record = value => value && typeof value === 'object' && !Array.isArray(value) ? value : {};

async function lookup(url, apiKey, fetchImpl) {
  const controller = new AbortController();
  let timer;
  try {
    return await Promise.race([
      (async () => {
        const response = await fetchImpl(url, {
          headers: { 'X-API-Key': apiKey, Accept: 'application/json' },
          signal: controller.signal, redirect: 'error',
        });
        if (!response.ok || !response.body) return null;
        const reader = response.body.getReader();
        const chunks = [];
        let size = 0;
        try {
          while (true) {
            const { done, value } = await reader.read();
            if (done) break;
            size += value.byteLength;
            if (size > 32768) { controller.abort(); return null; }
            chunks.push(value);
          }
        } finally {
          reader.releaseLock();
        }
        return record(JSON.parse(Buffer.concat(chunks).toString('utf8')));
      })(),
      // The deadline covers the request and its response body. No retries.
      new Promise(resolve => { timer = setTimeout(() => { controller.abort(); resolve(null); }, 5000); }),
    ]);
  } catch {
    return null;
  } finally {
    controller.abort();
    clearTimeout(timer);
  }
}

/**
 * Call after receiving a form, with values from the incoming HTTP request.
 * visitorIp must come from your host's configured, trusted request/proxy integration.
 * Never use the submitted hidden ip or blindly read X-Forwarded-For.
 * userAgent is the incoming User-Agent header; clients can change that header too.
 * Keep PARSEAPI_KEY on your server. Paid plans include these deep fields.
 * @param {{ visitorIp?: string | null, userAgent?: string | null }} request
 * @param {{ apiKey?: string, fetchImpl?: typeof fetch }} options
 */
export async function getFormContext(
  { visitorIp, userAgent },
  { apiKey = process.env.PARSEAPI_KEY, fetchImpl = fetch } = {},
) {
  apiKey = typeof apiKey === 'string' ? apiKey.trim() : '';
  if (!apiKey || apiKey.startsWith('parse_public_')) {
    throw new Error('Set PARSEAPI_KEY to your secret ParseAPI key on the server.');
  }
  const ip = typeof visitorIp === 'string' ? visitorIp.trim() : '';
  const ua = typeof userAgent === 'string' ? userAgent.trim() : '';
  const ipUrl = isIP(ip) && !ip.includes('%')
    ? new URL(`/ip/${encodeURIComponent(ip)}?deep=true`, 'https://api.parseapi.com') : null;
  const uaUrl = ua && ua.length <= 8192 && !/[\r\n]/.test(ua)
    ? new URL('/useragent', 'https://api.parseapi.com') : null;
  if (uaUrl) uaUrl.search = new URLSearchParams({ ua, deep: 'true' }).toString();

  const [ipAnswer, uaAnswer] = await Promise.all([
    ipUrl ? lookup(ipUrl, apiKey, fetchImpl) : null,
    uaUrl ? lookup(uaUrl, apiKey, fetchImpl) : null,
  ]);
  const location = record(ipAnswer);
  const network = record(location.deep);
  const agent = record(uaAnswer);
  const detail = record(agent.deep);

  // Allowlisted facts for your own rules. Unknown or plan-locked fields stay null.
  // false means not detected. Neither an IP location nor a User-Agent proves identity.
  return {
    ip: ipUrl ? ip : null,
    ip_country: text(location.country),
    ip_state: text(network.state),
    ip_city: text(network.city),
    ip_asn: text(location.asn),
    ip_vpn: flag(network.vpn),
    ip_tor: flag(network.tor),
    ip_relay: flag(network.relay),
    ip_datacenter: flag(network.datacenter),
    device: text(agent.device),
    os: text(agent.os),
    browser: text(agent.browser),
    bot: flag(agent.bot),
    bot_name: text(record(detail.bot).name),
  };
}

Make it yours.

What can each field do?

Email checks format, domain, and disposable status, with typo suggestions. They do not prove mailbox ownership or deliverability. Phone parsing standardizes the number; it does not check line activity. US address suggestions help with entry; selection does not verify delivery. Missing results stay empty.

By default, invalid or disposable results use the browser's validation to block submission. Set manual validation to handle that policy in your own code.

For measurements, set data-parse="measure" and a fixed target such as data-parse-to="cm". Optional data-parse-locale and data-parse-system use explicit source context.

Use data-parse-country="US" for US national phone numbers; a leading + keeps its own calling code. The demo keeps address country explicitly US. An IP button can suggest a country on click. Country choices can supply editable state suggestions.

Can I choose the answers and field names?

The input's name is what your backend receives. data-parse-fill chooses a response field. Outputs use the preceding marked input; use data-parse-from with a unique input id to connect explicitly. Dotted keys read nested answers. Visible fields work too.

For another included lookup, put an API path in data-parse with one placeholder for the input. Other path and query values stay fixed. Custom paths fill answers and leave validation to your form.

Your fields · HTML
<!-- Include form.js once, then add your own fields. -->
<input id="billing-country" name="country"
       data-parse="/country/{code}" placeholder="US">

<input type="hidden" name="billing_currency"
       data-parse-from="billing-country" data-parse-fill="currency">

<input type="hidden" name="calling_code"
       data-parse-from="billing-country" data-parse-fill="calling_code">

Supported custom paths: address, asn, bloc, city, company, continent, country, currency, date, district, elevation, email, emoji, holiday, iban, ip, language, mac, measure, name, npi, phone, point, postal, state, tariff, time, timezone, useragent, vat, vin. See API docs for paths and response fields. Unsupported paths are left alone.

Will it look and behave like my form?

form.js adds no stylesheet or inline styles. You supply the colors, layout, and field states. Default helpers provide suggestions and messages. Style .parse-hint, .parse-hint-suggestion, and .parse-address-suggestions with your own CSS.

Use data-parse-status for states and data-parse-reason for details. Set data-parse-ui="none" to render suggestions and messages with your own components. Set data-parse-validation="manual" to keep custom validity entirely in your code. Native HTML rules still apply.

Your messages and suggestions · HTML
<!-- Include form.js once. Your markup, CSS, and messages. -->
<input id="contact-email" type="email" name="email"
       data-parse="email" data-parse-ui="none"
       data-parse-validation="manual" aria-describedby="email-help">
<p id="email-help" role="status"></p>
<button id="email-correction" type="button" hidden></button>

<script>
  const email = document.getElementById("contact-email");
  const help = document.getElementById("email-help");
  const correction = document.getElementById("email-correction");
  const copy = {
    invalid: "Check the email format.",
    domain_invalid: "Check the domain after @.",
    disposable: "Use an email you plan to keep.",
    unavailable: "Email check unavailable. You can still send the form."
  };
  email.addEventListener("parse", ({ detail }) => {
    const invalid = ["invalid", "disposable"].includes(detail.status);
    const message = copy[detail.reason] ?? detail.message ?? "";
    help.textContent = message;
    email.setCustomValidity(invalid ? message : "");
    email.setAttribute("aria-invalid", String(invalid));
    correction.hidden = !detail.suggestions.length;
    correction.textContent = detail.suggestions[0]?.label ?? "";
    correction.onclick = () => detail.select?.(0);
  });
</script>

The bubbling parse event gives you status, value, answer, reason, message, suggestions, and select(index). Your UI owns focus, keyboard interaction, and accessibility. Set these options before attachment; stale selections are ignored after edits.

What happens if a lookup fails?

A missing script, rejected request, malformed reply, or timeout leaves the lookup unchecked. The script does not hold submission for pending enrichment. Your own required fields and validation still apply. Editing a source clears its connected generated answers, so old details do not ride along with new input.

How do visit details and pricing work?

Hidden data-parse="ip" and data-parse="useragent" sources opt into one lookup each after the first non-empty edit in that form. Opening the page or focusing a field makes no visit-context request. Set data-parse-deep="true" to include deep data in the same request. Deep IP and User-Agent data is included with paid plans. On Free, core fields still fill and deep fields stay empty. For flags, false means not detected.

All form.js lookups use your plan's request allowance. The script makes no metered verification calls. Explicit ?deep=true on custom paths is limited to included enrichment on ip, useragent, npi, company, tariff, point. Run mailbox verification and other metered checks on your backend after receiving the submission; see the email API reference.

Does it work with dynamic forms?

Call parseform.scan() after adding inputs. Existing fields stay attached once. Custom lookups report filled; address and city selections report selected. Neither status means an input has been verified. Keep unique input ids across your forms and include the script once.

One script.
Less typing.

Start includes 1M API requests a month for $20/mo.