Documentation

Flip OTP verifies a phone number by having the user send a short code from WhatsApp to your business number. This guide covers the widget, the proof token and the HTTP API.

Quickstart

One script tag, one input, one callback. That is the whole integration.

html
<script src="https://app.flipotp.com/flipotp.js"></script>

<input class="js-phone" name="phone" type="tel" placeholder="+60 12 345 6789" />

<script>
  FlipOTP.init({
    key: 'fpo_live_xxxxxxxxxxxxxxxx',
    input: '.js-phone',          // CSS selector, element, or array of either
    defaultCountry: 'MY',        // used when the user omits a country code
    tokenInput: 'flipotp_token', // hidden field the proof is written to
    onVerified: (token, { phone }) => {
      console.log('verified', phone, token);
    },
    onError: (message) => console.warn(message),
  });
</script>
The proof token is written into a hidden field named flipotp_token. Submit it with your form and verify it server-side — never trust the badge alone.

Install

Add the script to any page. The widget is a vanilla JavaScript bundle, isolated in a shadow DOM, and works with React, Vue, Svelte, Astro, Rails, Laravel and plain HTML.

html
<script src="https://app.flipotp.com/flipotp.js" defer></script>

You can also initialise declaratively by putting attributes on the script tag:

html
<script
  src="https://app.flipotp.com/flipotp.js"
  data-flipotp-key="fpo_live_xxxxxxxx"
  data-flipotp-input=".js-phone"
  data-flipotp-country="MY"
></script>

Widget options

  • key — your public site key. Required.
  • input — a CSS selector, an input element, or an array of either. Required.
  • defaultCountry — ISO country code applied when the number has no country prefix. Defaults to MY.
  • tokenInput — name of the hidden field that receives the proof. Defaults to flipotp_token.
  • apiBase — override the API origin. Only useful for self-hosted or staging.
  • onVerified(token, { phone }) — called when the number is verified.
  • onError(message) — called for validation and network errors.

The widget also emits a flipotp:verified DOM event on the phone input, with detail.token and detail.phone.

How verification works

  1. Clicking Verify asks the API for a 6-character code, bound to the phone number and your site.
  2. The code is stored for 100 seconds. One active code per number per site.
  3. The modal shows the code, a wa.me deep link and a QR code.
  4. The user sends that code from their own WhatsApp to the Flip OTP business number.
  5. The webhook accepts it only when the sender number equals the number being verified — this is the entire security model.
  6. The API signs a proof with your site secret and hands it to the widget.

Verify the proof

Tokens are base64url(payload).base64url(HMAC-SHA256), signed with your site secret. They expire after 15 minutes by default.

ts
// Node / any runtime with WebCrypto — no SDK required
import { verifyProof } from '@flipotp/core';

const result = await verifyProof(token, process.env.FLIPOTP_SITE_SECRET);
if (!result) return respond(400, 'Invalid or expired proof');
if (result.phone !== submittedPhone) return respond(400, 'Phone mismatch');
// result: { acc, site, phone, vid, iat, exp } — all identifiers are UUIDs

Prefer not to embed crypto? Validate online:

http
POST https://app.flipotp.com/v1/verify/token
{ "token": "<proof>" }

→ { "valid": true, "phone": "+60123456789", "siteUuid": "ste-…",
    "verificationUuid": "ver-…", "expiresAt": "…" }

API reference

Start a verification

http
POST https://app.flipotp.com/v1/verify/start
{ "key": "fpo_live_…", "phone": "+60123456789", "country": "MY" }

→ { "id": "ver-…", "code": "K7M2QP",
    "waLink": "https://wa.me/60123…?text=K7M2QP",
    "whatsappNumber": "60123…", "expiresAt": "…", "ttlSeconds": 100 }

GET https://app.flipotp.com/v1/verify/status?id=ver-…
→ { "status": "pending" | "verified" | "expired", "proof": "…" }

Errors

  • 400 — invalid phone number or missing fields.
  • 402 — verification quota reached. Upgrade to continue.
  • 403 — domain not allowed for this site key.
  • 404 — unknown site key.
  • 429 — rate limited. Retry after the window.

Limits

Defaults on the shared WhatsApp number: one active code per phone per site, five codes per phone per hour, 60 codes per site per minute and 600 per account per minute. Superadmins can tune every value, and plan limits are enforced before a code is issued.

WhatsApp setup

On the Dev and SME plans you send codes to the shared Flip OTP WhatsApp Business number. The number is shown in the modal and in every site's settings page. No Meta app review is needed on your side.

The SaaS plan can use a white-labelled or per-tenant WhatsApp number. Contact us to provision it.

Need a dashboard? Create a free account or open the live playground.