API documentation

Verify phone numbers via missed call, Telegram bot, or WhatsApp. One API, multiple providers.


Overview

  1. Your server sends a phone number to POST /verifications
  2. Veryfon initiates verification via the project's configured provider
  3. User gets a missed call or opens the Telegram bot
  4. Your server polls GET /verifications/:id or receives a webhook
  5. Verification is marked verified

Authentication

Include your API key in the Authorization header:

Authorization: Bearer vf_live_<your_api_key>

API keys are generated in the project dashboard. Key format: vf_live_ + 48 hex characters.


Endpoints

Create verification

POST
/verifications

Request body (JSON):

{
  "phone": "+37112345678"
}
Field Type Required Description
phonestringyesE.164 format, e.g. +37112345678
qr_codebooleannoIf true, response includes a qr_code_url

Response (201):

{
  "verification_id": "a1b2c3d4-...",
  "call_number": "+18005551234",
  "provider_used": "missed_call",
  "method": "call",
  "qr_code_url": "https://veryfon.com/qr/a1b2c3d4-....png"
}

Note: call_number depends on provider — a phone number (missed call) or a Telegram link (Telegram)

Check verification status

GET
/verifications/:id

Response (200):

{
  "verification_id": "a1b2c3d4-...",
  "phone": "+37112345678",
  "status": "verified",
  "credits_remaining": 499,
  "balance_cents": 49900,
  "provider_used": "missed_call",
  "created_at": "2026-07-24T12:00:00Z",
  "expires_at": "2026-07-24T12:05:00Z",
  "verified_at": "2026-07-24T12:00:30Z"
}

Status values: pending, verified, expired (5-minute TTL)

balance_cents — current account balance in cents (100 cents = 1 ₽). credits_remaining is balance_cents / 100 for backward compatibility.

Errors

HTTP Code Description
401AuthenticationErrorInvalid or missing API key
402InsufficientBalanceErrorProject balance too low
404NotFoundErrorVerification ID not found
422ValidationErrorInvalid phone, missing code, etc.
502ErrorUpstream provider error

Error body: {"error": "..."}


Webhooks

When a verification transitions from pending to verified, Veryfon sends a signed POST to your webhook URL.

Payload

{
  "verification_id": "a1b2c3d4-...",
  "phone": "+37112345678",
  "status": "verified",
  "verified_at": "2026-07-24T12:00:30Z"
}

Signature verification

Compute HMAC-SHA256 of the raw request body using your webhook secret, hex-encode, and compare with X-Veryfon-Signature using constant-time comparison.

Ruby (Veryfon gem)

require "veryfon"

v = Veryfon::Webhook.verify(request.body.read,
                            request.headers["X-Veryfon-Signature"],
                            ENV["VERYFON_WEBHOOK_SECRET"])
v.phone    # => "+37112345678"
v.status   # => "verified"

Ruby (raw)

require "openssl"

body   = request.body.read
sig    = request.headers["X-Veryfon-Signature"]
secret = ENV["VERYFON_WEBHOOK_SECRET"]

expected = OpenSSL::HMAC.hexdigest("SHA256", secret, body)
if expected == sig
  data = JSON.parse(body)
  # data["status"] == "verified"
else
  # reject
end

Python

import hmac, hashlib, json

body = request.data
sig  = request.headers["X-Veryfon-Signature"]
secret = os.environ["VERYFON_WEBHOOK_SECRET"].encode()

expected = hmac.new(secret, body, hashlib.sha256).hexdigest()
if hmac.compare_digest(expected, sig):
    data = json.loads(body)
    # data["status"] == "verified"

Node.js

import crypto from "crypto";

const body = JSON.stringify(req.body);
const sig  = req.headers["x-veryfon-signature"];
const expected = crypto
  .createHmac("sha256", process.env.VERYFON_WEBHOOK_SECRET)
  .update(body)
  .digest("hex");

if (crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))) {
  // req.body.status === "verified"
}

PHP

$body = file_get_contents("php://input");
$sig  = $_SERVER["HTTP_X_VERYFON_SIGNATURE"];
$secret = getenv("VERYFON_WEBHOOK_SECRET");

$expected = hash_hmac("sha256", $body, $secret);
if (hash_equals($expected, $sig)) {
  $data = json_decode($body, true);
  // $data["status"] === "verified"
}

Go

import (
  "crypto/hmac"
  "crypto/sha256"
  "encoding/hex"
  "encoding/json"
  "io"
  "os"
)

body, _ := io.ReadAll(r.Body)
sig := r.Header.Get("X-Veryfon-Signature")
secret := os.Getenv("VERYFON_WEBHOOK_SECRET")

mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))

if hmac.Equal([]byte(expected), []byte(sig)) {
  var data struct{ Status string `json:"status"` }
  json.Unmarshal(body, &data)
  // data.Status == "verified"
}

Full integration examples

curl

# 1. Create verification
RESP=$(curl -s -X POST https://veryfon.com/verifications \
  -H "Authorization: Bearer vf_live_..." \
  -H "Content-Type: application/json" \
  -d '{"phone": "+37112345678"}')

VID=$(echo "$RESP" | ruby -rjson -e 'puts JSON.parse(STDIN.read)["verification_id"]')

# 2. Poll until verified
while true; do
  STATUS=$(curl -s https://veryfon.com/verifications/$VID \
    -H "Authorization: Bearer vf_live_..." | \
    ruby -rjson -e 'puts JSON.parse(STDIN.read)["status"]')
  echo "Status: $STATUS"
  [ "$STATUS" = "verified" ] || [ "$STATUS" = "expired" ] && break
  sleep 2
done

Ruby (Sinatra)

# Gemfile
gem "veryfon"
gem "sinatra"

# app.rb
require "sinatra"
require "veryfon"

VERYFON = Veryfon::Client.new(api_key: ENV["VERYFON_API_KEY"])

post "/verify" do
  phone = params[:phone]

  # Step 1: create verification
  v = VERYFON.request_verification(phone: phone)

  # Step 2: wait for result (polls internally)
  v = VERYFON.wait_for_verification(v.id)

  if v.verified?
    "Phone #{phone} verified!"
  else
    "Verification expired"
  end
end

# Webhook endpoint (called by Veryfon when verification completes)
post "/webhooks/veryfon" do
  body   = request.body.read
  sig    = request.env["HTTP_X_VERYFON_SIGNATURE"]
  secret = ENV["VERYFON_WEBHOOK_SECRET"]

  v = Veryfon::Webhook.verify(body, sig, secret)
  puts "Verified: #{v.phone}"
  200
rescue Veryfon::SignatureMismatchError
  halt 401
end

Python (Flask)

# app.py
import os
import time
import requests
import hmac
import hashlib
from flask import Flask, request, jsonify

app = Flask(__name__)
API_KEY = os.environ["VERYFON_API_KEY"]
WEBHOOK_SECRET = os.environ["VERYFON_WEBHOOK_SECRET"]
BASE_URL = "https://veryfon.com"

def create_verification(phone):
    resp = requests.post(
        f"{BASE_URL}/verifications",
        headers={
            "Authorization": f"Bearer {API_KEY}",
            "Content-Type": "application/json",
        },
        json={"phone": phone},
    )
    resp.raise_for_status()
    return resp.json()

def check_verification(vid):
    resp = requests.get(
        f"{BASE_URL}/verifications/{vid}",
        headers={"Authorization": f"Bearer {API_KEY}"},
    )
    resp.raise_for_status()
    return resp.json()

def wait_for_verification(vid, timeout=300):
    deadline = time.time() + timeout
    while time.time() < deadline:
        v = check_verification(vid)
        if v["status"] in ("verified", "expired"):
            return v
        time.sleep(2)
    raise TimeoutError("Verification timed out")

@app.route("/verify", methods=["POST"])
def verify():
    data = request.get_json()
    v = create_verification(data["phone"])
    vid = v["verification_id"]
    result = wait_for_verification(vid)
    return jsonify(phone=data["phone"], status=result["status"])

@app.route("/webhooks/veryfon", methods=["POST"])
def webhook():
    body = request.data
    sig = request.headers.get("X-Veryfon-Signature")
    secret = WEBHOOK_SECRET.encode()

    expected = hmac.new(secret, body, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, sig):
        return "Invalid signature", 401

    data = request.get_json()
    print(f"Verified: {data['phone']}")
    return "OK", 200

Node.js (Express)

// app.js
import express from "express";
import crypto from "crypto";

const app = express();
const API_KEY = process.env.VERYFON_API_KEY;
const WEBHOOK_SECRET = process.env.VERYFON_WEBHOOK_SECRET;
const BASE_URL = "https://veryfon.com";

async function createVerification(phone) {
  const resp = await fetch(`${BASE_URL}/verifications`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ phone }),
  });
  return resp.json();
}

async function checkVerification(id) {
  const resp = await fetch(`${BASE_URL}/verifications/${id}`, {
    headers: { Authorization: `Bearer ${API_KEY}` },
  });
  return resp.json();
}

async function waitForVerification(id, timeout = 300) {
  const deadline = Date.now() + timeout * 1000;
  while (Date.now() < deadline) {
    const v = await checkVerification(id);
    if (v.status === "verified" || v.status === "expired") return v;
    await new Promise((r) => setTimeout(r, 2000));
  }
  throw new Error("Timed out");
}

app.post("/verify", express.json(), async (req, res) => {
  const v = await createVerification(req.body.phone);
  const result = await waitForVerification(v.verification_id);
  res.json({ phone: req.body.phone, status: result.status });
});

app.post("/webhooks/veryfon", express.text(), (req, res) => {
  const sig = req.headers["x-veryfon-signature"];
  const expected = crypto
    .createHmac("sha256", WEBHOOK_SECRET)
    .update(req.body)
    .digest("hex");

  if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))) {
    return res.status(401).send("Invalid signature");
  }

  const data = JSON.parse(req.body);
  console.log("Verified:", data.phone);
  res.status(200).send("OK");
});

app.listen(3000);

PHP

<?php
// index.php
require_once "vendor/autoload.php";

$apiKey = getenv("VERYFON_API_KEY");
$webhookSecret = getenv("VERYFON_WEBHOOK_SECRET");
$baseUrl = "https://veryfon.com";

function createVerification($phone) {
    global $apiKey, $baseUrl;
    $ch = curl_init("$baseUrl/verifications");
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_HTTPHEADER => [
            "Authorization: Bearer $apiKey",
            "Content-Type: application/json",
        ],
        CURLOPT_POSTFIELDS => json_encode(["phone" => $phone]),
        CURLOPT_RETURNTRANSFER => true,
    ]);
    $resp = curl_exec($ch);
    curl_close($ch);
    return json_decode($resp, true);
}

function checkVerification($vid) {
    global $apiKey, $baseUrl;
    $ch = curl_init("$baseUrl/verifications/$vid");
    curl_setopt_array($ch, [
        CURLOPT_HTTPHEADER => ["Authorization: Bearer $apiKey"],
        CURLOPT_RETURNTRANSFER => true,
    ]);
    $resp = curl_exec($ch);
    curl_close($ch);
    return json_decode($resp, true);
}

function waitForVerification($vid, $timeout = 300) {
    $deadline = time() + $timeout;
    while (time() < $deadline) {
        $v = checkVerification($vid);
        if (in_array($v["status"], ["verified", "expired"])) return $v;
        sleep(2);
    }
    throw new Exception("Timed out");
}

// Webhook endpoint
$body = file_get_contents("php://input");
$sig = $_SERVER["HTTP_X_VERYFON_SIGNATURE"] ?? "";
$expected = hash_hmac("sha256", $body, $webhookSecret);

if (hash_equals($expected, $sig)) {
    $data = json_decode($body, true);
    error_log("Verified: " . $data["phone"]);
    http_response_code(200);
} else {
    http_response_code(401);
}
?>

Go

package main

import (
    "bytes"
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
    "time"
)

var (
    apiKey        = os.Getenv("VERYFON_API_KEY")
    webhookSecret = os.Getenv("VERYFON_WEBHOOK_SECRET")
    baseURL       = "https://veryfon.com"
)

type Verification struct {
    ID        string `json:"verification_id"`
    Phone     string `json:"phone"`
    Status    string `json:"status"`
    CallNumber string `json:"call_number"`
}

func createVerification(phone string) (*Verification, error) {
    body, _ := json.Marshal(map[string]string{"phone": phone})
    req, _ := http.NewRequest("POST", baseURL+"/verifications", bytes.NewReader(body))
    req.Header.Set("Authorization", "Bearer "+apiKey)
    req.Header.Set("Content-Type", "application/json")

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    var v Verification
    json.NewDecoder(resp.Body).Decode(&v)
    return &v, nil
}

func checkVerification(id string) (*Verification, error) {
    req, _ := http.NewRequest("GET", baseURL+"/verifications/"+id, nil)
    req.Header.Set("Authorization", "Bearer "+apiKey)

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    var v Verification
    json.NewDecoder(resp.Body).Decode(&v)
    return &v, nil
}

func waitForVerification(id string, timeout time.Duration) (*Verification, error) {
    deadline := time.Now().Add(timeout)
    for time.Now().Before(deadline) {
        v, err := checkVerification(id)
        if err != nil {
            return nil, err
        }
        if v.Status == "verified" || v.Status == "expired" {
            return v, nil
        }
        time.Sleep(2 * time.Second)
    }
    return nil, fmt.Errorf("timed out")
}

func verifyWebhook(r *http.Request) (*Verification, error) {
    body, _ := io.ReadAll(r.Body)
    sig := r.Header.Get("X-Veryfon-Signature")

    mac := hmac.New(sha256.New, []byte(webhookSecret))
    mac.Write(body)
    expected := hex.EncodeToString(mac.Sum(nil))

    if !hmac.Equal([]byte(expected), []byte(sig)) {
        return nil, fmt.Errorf("invalid signature")
    }

    var v Verification
    json.Unmarshal(body, &v)
    return &v, nil
}

Providers

Each project selects a primary and optional fallback provider. Configured in the dashboard.

Provider Code Description
Missed callmissed_callUser receives a call; seeing the call confirms ownership
TelegramtelegramUser clicks a Telegram link, shares phone with the bot

Phone number format

E.164 format:

+[country code][number]
+37112345678
+12025551234
+447911123456

Spaces, hyphens, parentheses, and dots are stripped automatically. Must start with +.


Pricing

Balance-based billing (cents). Each verification deducts the provider's cost:

Provider Description
Missed call1 ₽
Telegram0.10 ₽

When balance is too low, API returns 402 InsufficientBalanceError. Top up in the dashboard.