@cubis/verifyme
| Install | |
|---|---|
npm install @cubis/verifyme |
|
| Latest Version: | 0.0.4 |
| Vue: | 3 |
| License: | MIT |
| Last Updated: | Sep 26, 2026 |
| Links: | npm |
@cubis/verifyme
Official SDK for Verifyme — send and verify one-time codes and magic links over SMS, Email, WhatsApp, Telegram and voice, embed the verification widget, and verify webhooks.
Zero runtime dependencies (fetch + Web Crypto). Works on Node 20+, Bun, Deno,
Cloudflare Workers and the browser. ESM and CommonJS, typed for both.
npm i @cubis/verifyme # bun add / pnpm add / yarn add
| Entry | Use it in | Needs an API key |
|---|---|---|
@cubis/verifyme |
your server | yes |
@cubis/verifyme/browser |
any web page | no |
@cubis/verifyme/react |
React 18+ | no |
@cubis/verifyme/vue |
Vue 3.3+ / Nuxt | no |
The API key sends messages, so it lives on your server only. Browsers get a per-session widget token instead.
Send and verify a code
import { Verifyme } from "@cubis/verifyme"
const verifyme = new Verifyme({ apiKey: process.env.VERIFYME_API_KEY! })
// or Verifyme.fromEnv() — reads VERIFYME_API_KEY and VERIFYME_URL
const { token, exp } = await verifyme.send({
target: "+85512345678",
provider: "sms", // sms | email | telegram, or "sms:twilio" to pin one profile
length: 6, // 4–8 digits
timeout: 300, // 10–300 seconds
template: "Your code is {{code}}",
})
// Keep `token` server-side (a session, a cache). The user only types the code.
const { success, message } = await verifyme.check({ token, code: "123456" })
check() resolves with success: false for a wrong, spent or expired code.
Anything else — a bad key, a firewall rule, a rate limit, an outage — throws.
import { VerifymeError } from "@cubis/verifyme"
try {
await verifyme.send({ target })
} catch (error) {
if (error instanceof VerifymeError) {
error.status // 429
error.retryAfter // seconds to wait
error.traceId // quote this in support; it matches the console audit log
error.retryable // 429, 5xx or a transport failure
}
}
Other calls:
await verifyme.status(token) // { status, target, created_at } — costs no attempt
await verifyme.otp.generate({ issuer: "Acme", account_name: "a@b.co" }) // TOTP
await verifyme.otp.verify({ secret, code })
await verifyme.botauth.create({ target, type: "telegram_bot" }) // Telegram login
Every method takes { signal } as a second argument, and the client takes
{ baseUrl, timeout, headers, fetch } — point baseUrl at your own instance
(http://localhost:8080 in development).
Widget
Three steps: your server opens a session, the browser runs the widget, your
server redeems the proof. Only the redeemed proof means verified — the
browser's onSuccess is a UI hint.
1. Server
const session = await verifyme.widget.createSession({
target: "+85512345678",
origin: "https://app.example.com", // must be allowlisted in the console
client_reference: userId, // returned to you on redeem
})
// → send session.widget_token to the page
2. Browser
import { openWidget } from "@cubis/verifyme/browser"
await openWidget({
token: widgetToken,
container: "#verify", // omit for a modal
onSuccess: ({ proof }) => fetch("/api/verified", { method: "POST", body: proof }),
})
React:
import { VerifyWidget } from "@cubis/verifyme/react"
;<VerifyWidget token={widgetToken} onSuccess={({ proof }) => redeem(proof)} />
or drive it yourself:
const { ref, status, error } = useVerifyWidget({ token, onSuccess })
return <div ref={ref} />
Vue (Nuxt: wrap it in <ClientOnly>):
<script setup lang="ts">
import { VerifyWidget } from "@cubis/verifyme/vue"
</script>
<template>
<VerifyWidget :token="widgetToken" @success="({ proof }) => redeem(proof)" />
</template>
useVerifyWidget({ token }) is the composable form; token may be a ref that
fills in later.
Svelte, Angular, Solid, plain HTML — once the loader is on the page
(loadWidgetScript() or the <script> tag) use the custom element:
<verifyme-widget token="vw_…"></verifyme-widget>
<script>
document.querySelector("verifyme-widget")
.addEventListener("verifyme-success", (e) => redeem(e.detail.proof))
</script>
Events: verifyme-ready, verifyme-success, verifyme-error,
verifyme-close. Set theme / labels as properties.
Faster first paint: the hooks start fetching the loader on mount, while your
server is still creating the session. Elsewhere call loadWidgetScript() early.
Strict CSP / pinned releases: every entry takes version and integrity
(from GET /widget/v1/integrity) to pin the loader with SRI, and nonce for a
nonce-based script-src. Allow frame-src for the Verifyme host.
3. Server
const result = await verifyme.widget.redeem(proof)
// { verified: true, target, client_reference, verification_id, mode, verified_at }
Look and feel
Branding is saved per business in Console → Widget → Customize and applies
to every session. Pass theme / labels only to override it for one embed:
openWidget({
token,
theme: {
mode: "auto", // auto | light | dark
font: "system", // system | serif | mono | rounded
density: "comfortable", // comfortable | compact
align: "center", // center | left
surface: "card", // card | plain (no border, blends into your page)
poweredBy: true, // false hides the "Powered by Verifyme" mark
radius: 12,
primary: "#4f46e5",
logo: "https://cdn.example.com/logo.svg",
},
labels: { title: "Verify your phone", submit: "Continue" },
})
Labels support {target}, {length}, {seconds} and {attempts} and are
rendered as plain text. The widget handle returned by openWidget has
update({ theme, labels }) and close(). height sets the iframe's initial
height (default 320 px) so the page does not shift while it loads.
Better Auth
Use @cubis/verifyme-better-auth — a separate
package, so this SDK stays dependency-free.
Webhooks
Verifyme signs every callback with X-Verifyme-Signature. Verify the raw
body — not a re-serialised object.
import { parseWebhook, type WidgetCallbackEvent } from "@cubis/verifyme"
const body = await request.text()
const event = await parseWebhook<WidgetCallbackEvent>({
payload: body,
signature: request.headers.get("x-verifyme-signature")!,
secret: process.env.VERIFYME_WEBHOOK_SECRET!, // whsec_… from the console
})
// event.session_id, event.target, event.client_reference, event.verified_at
parseWebhook throws on a bad or stale signature (5 minutes of tolerance, set
with toleranceSeconds). Use verifyWebhook for a boolean instead.
Notes
- Never log a code, a verification token or an API key.
- Codes are checked server-side; five wrong attempts burn the token.
- Test-mode keys never reach a provider. Add test targets in the console to return a fixed code and spend nothing in CI.
Development
bun install
bun test
bun run build # dist/ (ESM + types) and dist/cjs/
bun run release patch # bump, changelog, tag, publish — add --dry to preview
MIT © Cubis
Related Packages
Official K-OTP (Korean OTP API) SDK: issue and verify one-time passwords from br...
Idiomatic Vue headless WAI-ARIA one-time-code / PIN input (segmented cells, past...
Database-agnostic JWT authentication and communication bus for Node.js
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 0.0.4 | >=3.3 | MIT | |
| 0.0.2 | >=3.3 | MIT | |
| 0.0.1 | MIT |