API documentation
Verify phone numbers via missed call, Telegram bot, or WhatsApp. One API, multiple providers.
Overview
- Your server sends a phone number to
POST /verifications - Veryfon initiates verification via the project's configured provider
- User gets a missed call or opens the Telegram bot
- Your server polls
GET /verifications/:idor receives a webhook - 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
/verificationsRequest body (JSON):
{
"phone": "+37112345678"
}
| Field | Type | Required | Description |
|---|---|---|---|
phone | string | yes | E.164 format, e.g. +37112345678 |
qr_code | boolean | no | If 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
/verifications/:idResponse (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 |
|---|---|---|
| 401 | AuthenticationError | Invalid or missing API key |
| 402 | InsufficientBalanceError | Project balance too low |
| 404 | NotFoundError | Verification ID not found |
| 422 | ValidationError | Invalid phone, missing code, etc. |
| 502 | Error | Upstream 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 call | missed_call | User receives a call; seeing the call confirms ownership |
| Telegram | telegram | User 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 call | 1 ₽ |
| Telegram | 0.10 ₽ |
When balance is too low, API returns 402 InsufficientBalanceError. Top up in the dashboard.