Docs
Verify phones with WhatsApp
Your user sends one WhatsApp message and HiVouch tells you which number sent it. No code to type, nothing to intercept. Start in test mode with free keys, then go live after a short review.
How it works
- Your backend creates a verification with your secret key and gets back a WhatsApp link with a one-time code already in the message.
- Your user taps the link (or scans its QR code on a computer) and presses send in WhatsApp.
- HiVouch checks the code and the sender's number, replies in WhatsApp, and sends you a signed webhook.
- Your backend confirms the result and signs the user in.
Keys: sk_test_ and sk_live_ secret keys for your backend, pk_test_ and pk_live_ publishable keys for the widget. Test keys never send WhatsApp messages and are free. Base URL: https://api.hivouch.com/v1.
Widget
The quickest start: the widget creates the verification, shows the button or QR code, and reports the result. Confirm it on your backend with the verification id.
- Sign in with WhatsApp
purpose: "login" - For signing in. The widget and the WhatsApp reply say the user is signing in to your app. This is the default.
- Sign up with WhatsApp
purpose: "signup" - For creating an account. No number is typed (identify mode): the number that sends the message is verified, and the reply says the user is signing up.
- Verify your phone
purpose: "verify" - For a number you already collected: checkout, a profile, or a sign-up form with its own phone field. The wording says the user verified their number.
- Card
HiVouch.mountThe full card: number field, QR code or WhatsApp button, countdown and result, inline on your page.- Sign up with WhatsApp
HiVouch.buttonOne button, like Sign in with Google. It opens a dialog and the number that sends the message becomes the account. Nothing to type.- Verify a number you already have
HiVouch.verifyButtonA small button next to your own phone field. It checks that exact number is on WhatsApp and owned by the user, then shows Verified.
<script src="https://hivouch.com/widget.js"></script>
<div id="hivouch"></div>
<script>
HiVouch.mount("#hivouch", {
publishableKey: "pk_test_...", // add this site under Settings, Allowed origins
purpose: "login", // "login", "signup" (no number to type) or "verify"
phone: "+12025550110",
locale: "en",
// A signal for your UI only: confirm on your backend before trusting it.
onVerified: (v) => fetch("/verify/complete", { method: "POST", body: JSON.stringify({ id: v.id }) }),
onFailed: (v) => console.warn(v.status, v.message),
});
</script>Sign up with WhatsApp
<form action="/signup" method="post">
<div id="signup-whatsapp"></div>
<!-- once verified, the form gets <input type="hidden" name="hivouch_verification_id"> -->
</form>
<script>
HiVouch.button("#signup-whatsapp", {
publishableKey: "pk_test_...",
purpose: "signup", // or "login" for a Sign in with WhatsApp button
block: true, // full width
onVerified: (v) => document.querySelector("form").submit(),
});
</script>Verify a number you already have
The message must come from the number in the field, so a typo or someone else's number ends as a mismatch. Editing the number after it was verified brings the button back.
<form action="/profile" method="post">
<input id="phone" name="phone" type="tel" value="+20 100 555 0142">
<span id="verify-phone"></span>
</form>
<script>
HiVouch.verifyButton("#verify-phone", {
publishableKey: "pk_test_...",
phoneInput: "#phone", // or phone: "+201005550142" when there is no field
variant: "outline",
onVerified: (v) => console.log("verified", v.id),
onReset: () => console.log("number edited, verify again"),
});
</script>Match your design
Colors, corners and font from Settings, Branding apply to every widget and the hosted page. Each widget also takes size, width and theme, which win over the dashboard branding on that page. Values that are not valid CSS are ignored.
HiVouch.mount("#hivouch", {
publishableKey: "pk_test_...",
size: "sm", // "sm", "md" (default) or "lg"
width: "100%", // px number or CSS length, default 360px
theme: {
primaryColor: "#0a7d5a",
primaryTextColor: "#ffffff",
cardColor: "#ffffff",
textColor: "#14211c",
linkColor: "#0a7d5a",
radius: 8, // 0 to 32; buttons are pills from 20 up
font: '"Cairo", sans-serif', // load the font on your page
},
});Every callback is a signal for your interface. On your backend, read the verification with your secret key (GET /v1/verifications/{id}) and check that it is verified and its verified_phone is the number you expect before you create the account or save the number.
Build your own UI
Skip the widget and design the screen yourself. Your backend creates a verification, your page shows the WhatsApp button or QR code, and your backend confirms the result. HiVouch issues the code, receives the message and tells you who sent it.
Create a verification on your backend
Call the API with your secret key and the user's phone number in E.164 format. Put your own user id in metadata so you can match the result later.
// Server side only: the secret key must never reach the browser or the app.
const res = await fetch("https://api.hivouch.com/v1/verifications", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.HIVOUCH_SECRET_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({ phone: "+12025550110", purpose: "verify", locale: "en", metadata: { user_id: "u_123" } }),
});
if (!res.ok) throw new Error((await res.json()).error.message);
const v = await res.json();
// Send these to your page: v.id, v.wa_link, v.greeting, v.token_display, v.hivouch_number, v.expires_atThe response has everything your screen needs:
{
"id": "ver_01JC8Z4Q7M...",
"object": "verification",
"status": "pending",
"mode": "confirm",
"purpose": "verify",
"phone": "+12025550110",
"hivouch_number": "+12025550100",
"token_display": "VCH-7KQ2-9XWD-M4TA",
"greeting": "Hi Acme, it's me. VCH-7KQ2-9XWD-M4TA",
"wa_link": "https://wa.me/12025550100?text=Hi%20Acme%2C%20it's%20me.%20VCH-7KQ2-9XWD-M4TA",
"client_secret": "vcs_...",
"expires_at": "2026-11-08T14:07:00Z",
"livemode": false,
"metadata": { "user_id": "u_123" }
}Show it on your page
Pass wa_link, greeting, hivouch_number and expires_at to your page. Never the secret key.
- On a phone, a button that opens
wa_link: WhatsApp opens with the message typed, the user presses send. - On a computer, a QR code of
wa_linkfor the phone camera. - A fallback line with the greeting and number to copy, and a countdown to
expires_atwith a Start again button.
<!-- Phone: one tap opens WhatsApp with the message already typed. -->
<a href="{wa_link}" class="btn">Verify with WhatsApp</a>
<!-- Desktop: show wa_link as a QR code for the phone camera (any QR library). -->
<canvas id="qr"></canvas>
<script type="module">
import QRCode from "https://esm.sh/[email protected]";
QRCode.toCanvas(document.getElementById("qr"), "{wa_link}");
</script>
<!-- Fallback: let the user send the message by hand. -->
<p>Send <code>{greeting}</code> on WhatsApp to <strong>{hivouch_number}</strong></p>
<p>Expires at {expires_at}</p>Use the greeting and link exactly as returned. Do not write or change the code yourself: it is tied to this one verification and the number it was issued for.
Wait for the result
Your page polls your own backend every couple of seconds (and again when the tab becomes visible, because the user comes back from WhatsApp). Your backend reads the verification from HiVouch.
// Your page asks YOUR backend every 2 seconds; your backend asks HiVouch.
app.get("/verify/:id/status", async (req, res) => {
const r = await fetch(`https://api.hivouch.com/v1/verifications/${req.params.id}`, {
headers: { Authorization: `Bearer ${process.env.HIVOUCH_SECRET_KEY}` },
});
const v = await r.json();
if (v.status === "verified" && v.metadata?.user_id === req.session.pendingUserId) {
// Trust only this server-side check. Use v.verified_phone, not anything the browser sent.
await markPhoneVerified(req.session.pendingUserId, v.verified_phone);
}
res.json({ status: v.status });
});Receive the webhook
Add an endpoint under Developers, Webhooks. Every event carries a HiVouch-Signature header; check it against the raw body before you trust the event. Delivery is at least once, so ignore event ids you have already handled.
import crypto from "node:crypto";
// Header: HiVouch-Signature: t=1794060123,v1=5f3c... (HMAC-SHA256 of "{t}.{raw body}")
export function verifyHiVouch(rawBody, header, secret) {
const parts = Object.fromEntries((header || "").split(",").map((p) => p.split("=", 2)));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false; // reject anything older than 5 minutes
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const given = String(parts.v1 || "");
return given.length === expected.length && crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected));
}
app.post("/webhooks/hivouch", express.raw({ type: "application/json" }), (req, res) => {
const body = req.body.toString("utf8");
if (!verifyHiVouch(body, req.get("HiVouch-Signature"), process.env.HIVOUCH_WEBHOOK_SECRET)) return res.sendStatus(400);
const event = JSON.parse(body); // deduplicate by event.id: delivery is at least once
if (event.type === "verification.verified") {
const v = event.data.object; // v.id, v.verified_phone, v.metadata
}
res.sendStatus(200);
});Test without WhatsApp
With a test key, these numbers resolve by themselves. Test verifications are free.
+12025550110verifies right after it is created (the create response still says pending; read it again or wait for the webhook)+12025550111ends in mismatch, as if a different number sent the message+12025550112never gets a message and stays pending until it expires
Or create one for any number and make it verify:
curl -X POST https://api.hivouch.com/v1/test/verifications/{id}/simulate \
-H "Authorization: Bearer sk_test_..."Rules that keep it safe
- The secret key stays on your server. Browsers and apps never call the API with it.
- Only your backend decides a phone is verified, from the API or a signed webhook. Never from something the page reports.
- Use
verified_phonefrom HiVouch, and check the verification belongs to the user or session that started it. - For sign-in and sign-up, if
verified_phoneis null the sender's WhatsApp username hides their number: sign them in bywhatsapp_user_idinstead, and never treat an empty phone as an identity.whatsapp_usernameis for display only: users can change it, so never sign anyone in by it. - Each verification signs in one session. Mark its id as used.
Statuses
pending- Waiting for the user to send the WhatsApp message. Show the button, QR code and countdown.
verified- The right number sent the code. Confirm on your backend, then sign the user in.
mismatch- A different WhatsApp number sent the code. Ask the user to check the number and start again.
expired- Nothing arrived before expires_at. Offer to start again.
canceled- You canceled it with POST /v1/verifications/{id}/cancel.
disputed- The phone owner replied NO after a verification. Treat the sign-in as not trusted; the charge is refunded.
Integrate with an AI coding agent
Using an AI coding agent? Copy this prompt into it from your project folder. It describes the API, the screen to build, the security checks and the tests, and asks the agent to show you its plan before changing anything.
Give the agent your test key through an environment variable (HIVOUCH_SECRET_KEY), not in the chat.
Integrate HiVouch WhatsApp phone verification into this codebase.
## What HiVouch does
HiVouch verifies a phone number by having the user SEND a WhatsApp message (reverse OTP). My backend creates a verification; HiVouch returns a one-time code inside a prefilled WhatsApp message. The user taps a button (or scans a QR code), WhatsApp opens with the message typed, they press send, and HiVouch marks the verification as verified. No code is typed back into my app.
Full docs: https://hivouch.com/docs
## Before writing code
1. Read the project and tell me which backend framework, frontend framework and auth/session system it uses, and where phone verification belongs (sign-up, login, or adding a phone to an account).
2. Propose the files you will add or change and wait for my OK.
## API (base https://api.hivouch.com/v1, JSON, Bearer auth)
- Secret key: env var HIVOUCH_SECRET_KEY (sk_test_... or sk_live_...). Backend only. Never send it to the browser or mobile app, never commit it. Add it to the project's env example file with an empty value.
- Webhook signing secret: env var HIVOUCH_WEBHOOK_SECRET (whsec_...).
- POST /verifications body: {"phone": "+E164", "purpose": "login"|"signup"|"verify", "locale": "en"|"ar", "expires_in": 60-900 (default 300), "metadata": {"user_id": "..."}}. Send an "Idempotency-Key" header (a new UUID per attempt).
Response 201: id, status ("pending"), hivouch_number, token_display, greeting, wa_link, client_secret, expires_at, metadata, livemode.
purpose sets the wording the user sees in WhatsApp and on the page: "login" (default) says they are signing in, "signup" says they are signing up, "verify" says they verified their number. Use "signup" for creating an account (leave out phone for identify mode: the sender's number becomes the account), "verify" for checkout or a phone the user already gave you, "login" for signing in.
Omitting phone creates an "identify" verification (if identify is on in project settings): whoever sends the message is identified, and verified_phone tells you the number. If the sender's WhatsApp username hides their phone, purpose "login" or "signup" still verifies with verified_phone null and whatsapp_user_id set; purpose "verify" and confirm mode cannot complete without the phone.
- GET /verifications/{id} returns the verification. While pending it still includes wa_link, greeting and token_display; after that they are null. When verified: verified_phone (null when a WhatsApp username hides it), whatsapp_user_id, whatsapp_username (null if they have none), verified_at. Identify users by whatsapp_user_id or verified_phone only; whatsapp_username is for display and as a matching hint, because it can change and an old name can later belong to someone else.
- POST /verifications/{id}/cancel cancels a pending verification.
- Errors: {"error": {"type", "code", "message", "param"}} with HTTP 400/401/402/403/404/409/429. Show the message to developers in logs, and a friendly message to users. On 429 respect the Retry-After header.
## Statuses
- pending: Waiting for the user to send the WhatsApp message. Show the button, QR code and countdown.
- verified: The right number sent the code. Confirm on your backend, then sign the user in.
- mismatch: A different WhatsApp number sent the code. Ask the user to check the number and start again.
- expired: Nothing arrived before expires_at. Offer to start again.
- canceled: You canceled it with POST /v1/verifications/{id}/cancel.
- disputed: The phone owner replied NO after a verification. Treat the sign-in as not trusted; the charge is refunded.
## Build it like this
1. Backend endpoint "start verification": validate the phone as E.164, call POST /verifications with metadata.user_id (or the pending sign-up id), store the verification id against the user/session, and return to the frontend ONLY: id, wa_link, greeting, token_display, hivouch_number, expires_at. Never return the secret key.
2. Frontend screen:
- Mobile: a primary "Verify with WhatsApp" button linking to wa_link.
- Desktop: a QR code of wa_link (use a small QR library), plus the same button.
- Fallback text: "Send <greeting> to <hivouch_number> on WhatsApp", with a copy button.
- A countdown to expires_at, and a "Start again" action that creates a new verification.
- Use the greeting and wa_link exactly as returned. Never build, change or generate the code yourself: HiVouch issues it and ties it to this one verification.
3. Wait for the result, both ways:
- Webhook: POST endpoint that reads the RAW body, verifies the HiVouch-Signature header (format "t=<unix>,v1=<hex>", v1 = HMAC-SHA256(HIVOUCH_WEBHOOK_SECRET, "<t>.<raw body>"), constant-time compare, reject if t is older than 300 seconds), deduplicates by event id, and handles verification.verified, verification.mismatch, verification.expired, verification.canceled, verification.disputed. Return 2xx within 10 seconds.
- Polling fallback: the frontend asks MY backend for the status every 2 seconds while the screen is open (and again when the tab becomes visible, because the user comes back from WhatsApp); my backend calls GET /verifications/{id}.
4. Complete: only my backend decides. When status is "verified", check that the verification id belongs to this user/session and that metadata matches, then use verified_phone from HiVouch (never a phone number sent by the browser) to mark the phone verified or sign the user in. For sign-in or sign-up, if verified_phone is null, find or create the user by whatsapp_user_id instead (store it next to the phone when both are present); never treat a null or empty phone as an identity. Mark the verification id as used so it cannot complete a second session.
5. On "disputed" (the phone owner replied NO), revoke or flag the session or phone that verification created.
## Testing (test keys only, free, no WhatsApp needed)
Use an sk_test_ key and these numbers:
- +12025550110: verifies right after it is created (the create response still says pending; read it again or wait for the webhook)
- +12025550111: ends in mismatch, as if a different number sent the message
- +12025550112: never gets a message and stays pending until it expires
Or create a verification for any number and call POST https://api.hivouch.com/v1/test/verifications/{id}/simulate to make it verify.
Add automated tests that mock the HiVouch HTTP calls for: start, verified, mismatch, expired, a bad webhook signature, a replayed webhook, and a verification id that belongs to another user.
## Done means
- The secret keys are only in env vars and only used server side.
- The flow works end to end with sk_test_ and +12025550110.
- Webhook signatures are checked, and nothing trusts the browser's word that a phone is verified.
- Tell me which env vars to set and where to add the webhook URL (HiVouch dashboard > Developers > Webhooks).Sign in with WhatsApp on your login platform
HiVouch is a standard OpenID Connect provider, so login platforms can offer "Sign in with WhatsApp" next to Google or Apple. The user proves their number with one WhatsApp message and your platform gets an ID token with sub, phone_number and phone_number_verified. There is no email.
- In the dashboard open Developers, OIDC, and pick your platform under Connect your login platform.
- Enter its address (your Auth0 domain, Supabase project URL, Firebase auth domain or app URL) and press Create. HiVouch registers the right callback and settings for that platform.
- Copy the values shown, now with your client ID and secret, into the platform. The secret is shown once.
Issuer: https://verify.hivouch.com. Scopes: openid phone. Code flow with PKCE (S256).
- Auth.js (NextAuth)
- Add HiVouch as an OIDC provider in auth.ts. The dashboard gives you the file.
- Auth0
- Enterprise connection, OpenID Connect, back channel.
- Supabase
- Custom OIDC provider (identifier custom:hivouch) with email optional.
- Firebase
- OpenID Connect provider oidc.hivouch, code flow. Needs Identity Platform.
- Keycloak
- Identity provider of type OpenID Connect v1.0. Tested end to end.
- Amazon Cognito
- OIDC identity provider. Do not make email a required attribute.
- Any OIDC library
- Discovery URL, client ID and client secret are usually all it needs.
Clerk and Okta need an email from the provider, so they can't use HiVouch as a login provider yet. With them, verify the phone through the API or widget on your server and update the user there.
API reference
JSON over HTTPS with Authorization: Bearer sk_.... Send an Idempotency-Key header on POST requests to retry safely.
| Method | Path | What it does |
|---|---|---|
| POST | /v1/verifications | Create a verification. Body: phone (E.164; leave out for identify mode), purpose (login, signup or verify), locale, expires_in (60 to 900 seconds), return_url, metadata. |
| GET | /v1/verifications/{id} | Read one verification. While pending it includes wa_link, greeting and token_display. |
| POST | /v1/verifications/{id}/cancel | Cancel a pending verification. |
| GET | /v1/verifications | List verifications. Filters: status, phone, created_after, created_before. Cursor pagination. |
| POST | /v1/test/verifications/{id}/simulate | Test keys only: act as if the user sent the message. |
| GET | /v1/pricing | The public price list. No key needed. |
Webhook events
Signed with HiVouch-Signature: t=<unix>,v1=<hex>, where v1 is HMAC-SHA256 of <t>.<raw body> with your endpoint secret. Retries for 24 hours; return 2xx within 10 seconds.
verification.verifiedverification.mismatchverification.expiredverification.canceledverification.disputedbalance.lowbalance.depletednumber.status_changed
Errors
Every error has the same shape: {"error": {"type", "code", "message", "param"}}.
- 400
- phone_invalid, phone_required, expires_in_out_of_range
- 401
- invalid_api_key
- 402
- insufficient_balance
- 403
- project_not_live, country_not_enabled
- 404
- verification_not_found
- 409
- verification_not_pending
- 429
- rate_limited, phone_rate_limited, daily_limit_reached (see the Retry-After header)
Need help?
Email [email protected]. Signed in? Your keys, webhooks and request log are under Developers in the dashboard.