@cubis/verifyme

Verifyme SDK for JavaScript and TypeScript: send and verify one-time codes over SMS, Email and Telegram, embed the verification widget (React, Vue or any framework), and verify webhooks.
482
Install
npm install @cubis/verifyme
Latest Version:0.0.4
Vue:3
License:MIT
Last Updated:Sep 26, 2026
Links: npm
Maintainer: phumrin

@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

@k-otp/sdk

Official K-OTP (Korean OTP API) SDK: issue and verify one-time passwords from br...

1,010 0
vue-web-otp

Verify phone numbers on the vue web applications

2,708 4
@rozie-ui/otp-vue

Idiomatic Vue headless WAI-ARIA one-time-code / PIN input (segmented cells, past...

1,472 1
@veryai/widget

Cross-framework widget for Very palm verification.

20,956
@awesome-lang-auth/node

Database-agnostic JWT authentication and communication bus for Node.js

1,512 5

Version History

Version Released Vue License
0.0.4 >=3.3 MIT
0.0.2 >=3.3 MIT
0.0.1 MIT