Documentation

Website Integration - Server-side

Add missed-call phone verification to any website or CRM: create sessions from your backend, show the gateway number, poll or receive webhooks, and never expose your secret key.

Secret keys stay on your server

Never put sk_live_… or sk_test_… in browser JavaScript, a mobile app, or a public repository. The browser should only talk to your backend. Your backend calls Shernova with the secret key.

End-to-end flow

The user never installs an app. Your server creates the session, your page shows the gateway number, and the call itself proves ownership. A number is only trusted when the session status returned by the API (or a signed webhook) is verified or completed.

1. Create a session

Send the user's phone in E.164 format and a stable client_request_id so retries are idempotent. Keep the secret key in an environment variable.

Node.js 20+ (no dependencies)
const API = 'https://api.shernova.com';

export async function createVerification(phoneNumber, requestId) {
  const res = await fetch(API + '/v1/verifications', {
    method: 'POST',
    headers: {
      Authorization: 'Bearer ' + process.env.SHERNOVA_SECRET_KEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      phone_number: phoneNumber,
      client_request_id: requestId,
    }),
  });
  const data = await res.json();
  if (!res.ok) throw Object.assign(new Error(data.message), { data });
  return data;
}
Python (requests)
import os, requests

def create_verification(phone_number: str, request_id: str) -> dict:
    response = requests.post(
        "https://api.shernova.com/v1/verifications",
        headers={"Authorization": f"Bearer {os.environ['SHERNOVA_SECRET_KEY']}"},
        json={"phone_number": phone_number, "client_request_id": request_id},
        timeout=20,
    )
    response.raise_for_status()
    return response.json()
PHP (cURL)
<?php
function create_verification(string $phone, string $requestId): array {
    $ch = curl_init('https://api.shernova.com/v1/verifications');
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => [
            'Authorization: Bearer ' . getenv('SHERNOVA_SECRET_KEY'),
            'Content-Type: application/json',
        ],
        CURLOPT_POSTFIELDS => json_encode([
            'phone_number' => $phone,
            'client_request_id' => $requestId,
        ]),
    ]);
    $body = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    if ($status >= 400) throw new RuntimeException((string) $body);
    return json_decode((string) $body, true);
}

2. Handle queued and waiting correctly

gateway_phone_number is null while the session is queued because no verification line is free yet. Never show a placeholder number or let the user call before status === "waiting". The response for a waiting session looks like this:

Waiting session
{
  "session_id": "8f14e45f-ea3c-4f3f-a1c2-4d7c8b6a9f01",
  "status": "waiting",
  "phone_number": "+201012345678",
  "gateway_phone_number": "+201001234567",
  "qr_code_data": "tel:+201001234567",
  "environment": "live",
  "expires_at": "2026-10-21T12:05:00.000Z"
}
  • Poll every 2–4 seconds while status is queued or waiting.
  • When status becomes waiting, show the number as a tel: link and ask the user to ring once and hang up.
  • If the session expires, create a new one. Expired sessions cannot be revived.
  • Let the user cancel a queued/waiting session when they change their mind so the line is released.

qr_code_data is a tel: URI for the same gateway number. Render it as a QR code next to the number so desktop users can scan it with the phone that will place the call — no typing required. It is null while queued and after the session leaves waiting.

3. Poll status and cancel

Poll and cancel
export async function getVerification(sessionId) {
  const res = await fetch(API + '/v1/verifications/' + encodeURIComponent(sessionId), {
    headers: { Authorization: 'Bearer ' + process.env.SHERNOVA_SECRET_KEY },
  });
  return res.json();
}

export async function cancelVerification(sessionId) {
  const res = await fetch(API + '/v1/verifications/' + encodeURIComponent(sessionId), {
    method: 'DELETE',
    headers: { Authorization: 'Bearer ' + process.env.SHERNOVA_SECRET_KEY },
  });
  return res.json();
}
Status and cancel
curl "https://api.shernova.com/v1/verifications/SESSION_UUID" \
  -H "Authorization: Bearer sk_live_YOUR_SECRET_KEY"

curl -X DELETE "https://api.shernova.com/v1/verifications/SESSION_UUID" \
  -H "Authorization: Bearer sk_live_YOUR_SECRET_KEY"

4. Treat verified as success

In the current implementation verified already means the caller's number matched the session and is safe to accept. completed means the session was also delivered to a webhook. Do not wait for completed when you are polling: accept verified immediately and make your database update idempotent.

StatusMeaningWhat your website should do
queuedNo line is free yet; gateway number is nullKeep polling, show a waiting message
waitingA line is assigned; show the gateway numberAsk for one missed call
verifiedThe call matched; ownership is provenAccept the verification, store the result
completedWebhook delivery completedTreat as verified too (idempotent)
expiredThe call window closedOffer a new attempt
failed / rejectedThe call could not verify the numberShow retry
cancelledYou or the user cancelledOffer a new attempt

5. Prefer webhooks in production

Webhooks remove polling from your critical path. Verify the X-Shernova-Signature HMAC against the raw request body before parsing JSON, then handle the event idempotently. See Webhooks for the event schema and retries.

Express raw-body verification
import express from 'express';
import crypto from 'node:crypto';

const app = express();
app.post('/webhooks/shernova', express.raw({ type: 'application/json' }), (req, res) => {
  const received = String(req.headers['x-shernova-signature'] || '').replace(/^sha256=/, '');
  const expected = crypto
    .createHmac('sha256', process.env.SHERNOVA_WEBHOOK_SECRET)
    .update(req.body)
    .digest('hex');

  const a = Buffer.from(expected);
  const b = Buffer.from(received);
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    return res.status(401).send('invalid signature');
  }

  const event = JSON.parse(req.body.toString());
  if (event.status === 'verified' || event.status === 'completed') {
    // Mark the phone/user verified in your own database.
  }
  res.sendStatus(200);
});
Flask raw-body verification
import hmac, hashlib, os
from flask import Flask, request

app = Flask(__name__)

@app.post("/webhooks/shernova")
def shernova_webhook():
    raw = request.get_data()
    received = request.headers.get("X-Shernova-Signature", "").removeprefix("sha256=")
    expected = hmac.new(
        os.environ["SHERNOVA_WEBHOOK_SECRET"].encode(), raw, hashlib.sha256
    ).hexdigest()
    if not hmac.compare_digest(expected, received):
        return "invalid signature", 401
    event = request.get_json(force=True)
    if event.get("status") in ("verified", "completed"):
        pass  # mark verified in your database
    return "", 200

6. Errors your backend must handle

HTTPError codeMeaning
400invalid_phoneNot a valid E.164 phone number
401unauthorizedMissing or invalid secret key
403phone_verification_requiredShernova account owner must verify their own phone first
409phone_verification_in_progressAnother pending session exists for this phone
429verification_rate_limitedPer-phone/IP/device limits; respect retry_after_seconds
503maintenance_modeVerification is paused; show the bilingual message

Retries with the same client_request_id return the same session instead of creating a duplicate. Use a UUID from your own order/signup record.

7. Security and capacity checklist

  • Keep sk_* on the server only; proxy create/status/cancel through your backend.
  • Rate-limit your own start endpoint per authenticated user, not just per IP.
  • Accept only verified or completed; never trust a browser flag.
  • Validate and normalize phone numbers before sending them.
  • Use HTTPS and verify webhook signatures on the raw body.
  • Do not create a second session for a phone that already has an active one; reuse it.
  • Clean up abandoned sessions by cancelling or letting them expire.

8. Runnable examples

The repository contains dependency-free Node.js, PHP, and Python examples in examples/web-integration/. Each server exposes start, status, cancel, and webhook routes and reads the secret key from the environment. Start with the README in that folder.