Documentation

Verification API

Create a verification session from your backend with your secret key (sk_live_…). Users place a missed call to the gateway number returned in the response.

cURL
curl -X POST "https://api.shernova.com/v1/verifications" \
  -H "Authorization: Bearer sk_live_YOUR_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"phone_number":"+201012345678","client_request_id":"signup-001"}'
Node.js
const res = await fetch('https://api.shernova.com/v1/verifications', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.SHERNOVA_SECRET_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    phone_number: '+201012345678',
    client_request_id: 'signup-001',
  }),
});
const session = await res.json();
console.log('Call:', session.gateway_phone_number);
Python
import os, requests

resp = requests.post(
    "https://api.shernova.com/v1/verifications",
    headers={"Authorization": f"Bearer {os.environ['SHERNOVA_SECRET_KEY']}"},
    json={"phone_number": "+201012345678", "client_request_id": "signup-001"},
    timeout=30,
)
session = resp.json()
print("Call:", session["gateway_phone_number"])
PHP
$ch = curl_init('https://api.shernova.com/v1/verifications');
curl_setopt_array($ch, [
  CURLOPT_POST => true,
  CURLOPT_HTTPHEADER => [
    'Authorization: Bearer ' . getenv('SHERNOVA_SECRET_KEY'),
    'Content-Type: application/json',
  ],
  CURLOPT_POSTFIELDS => json_encode([
    'phone_number' => '+201012345678',
    'client_request_id' => 'signup-001',
  ]),
  CURLOPT_RETURNTRANSFER => true,
]);
$session = json_decode(curl_exec($ch), true);

Test keys (pk_test_ / sk_test_)

Use sk_test_ for backend test flows. Sessions use real gateways — call gateway_phone_number from the user's phone to complete:

Poll session status
curl "https://api.shernova.com/v1/verifications/SESSION_UUID" \
  -H "Authorization: Bearer sk_test_YOUR_KEY"

Receipt verification (backend)

Verify receipt once
const res = await fetch('https://api.shernova.com/v1/receipts/verify', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.SHERNOVA_SECRET_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ receipt: receiptJwtFromMobile }),
});
if (res.status === 409) {
  // receipt_already_used — replay attack
}

Flutter SDK

Complete sign-up flow
import 'package:flutter/material.dart';
import 'package:shernova_core/shernova_core.dart';
import 'package:shernova_ui/shernova_ui.dart';
import 'shernova_config.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Shernova.init(shernovaConfig);
  runApp(const PhoneVerifyPage());
}

class PhoneVerifyPage extends StatefulWidget {
  const PhoneVerifyPage({super.key});
  @override
  State<PhoneVerifyPage> createState() => _PhoneVerifyPageState();
}

class _PhoneVerifyPageState extends State<PhoneVerifyPage> {
  final _phone = TextEditingController();

  Future<void> _verify() async {
    try {
      final session = await verifyPhone(
        context: context,
        phoneNumber: _phone.text.trim(),
        clientRequestId: 'signup-${DateTime.now().millisecondsSinceEpoch}',
      );
      if (session.status == 'verified' && mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Verified: ${session.sessionId}')),
        );
      }
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text(e.toString())),
        );
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Verify phone')),
      body: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(children: [
          TextField(controller: _phone, decoration: const InputDecoration(labelText: 'Phone (+20…)')),
          const SizedBox(height: 16),
          FilledButton(onPressed: _verify, child: const Text('Verify')),
        ]),
      ),
    );
  }
}

Backend Example (Node.js)

Create verification + poll
const API = 'https://api.shernova.com';
const API_KEY = process.env.SHERNOVA_SECRET_KEY;

async function createVerification(phoneNumber) {
  const res = await fetch(`${API}/v1/verifications`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ phone_number: phoneNumber }),
  });
  if (!res.ok) throw new Error(await res.text());
  return res.json();
}

async function getSession(sessionId) {
  const res = await fetch(`${API}/v1/verifications/${sessionId}`, {
    headers: { Authorization: `Bearer ${API_KEY}` },
  });
  return res.json();
}

// Usage:
const session = await createVerification('+201012345678');
console.log('Call:', session.gateway_phone_number);

// Poll until verified (prefer webhooks in production)
let status = session.status;
while (status === 'waiting') {
  await new Promise(r => setTimeout(r, 3000));
  ({ status } = await getSession(session.session_id));
}
console.log('Final status:', status);

Webhook Handler Example

Verify signatures against the raw request body. With Express, mount express.raw({ type: 'application/json' }) on the webhook route before any JSON parser.

Express.js
const express = require('express');
const crypto = require('crypto');
const app = express();

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

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

  const event = JSON.parse(req.body.toString());
  if (event.status === 'verified') {
    // Mark user as verified in your database
    console.log('Verified:', event.session_id, event.phone_number);
  }
  res.sendStatus(200);
});

Do not use the legacy shernova package (1.x). Required: shernova_core. Optional: shernova_ui, shernova_cli (dev).

Webhooks

Verify HMAC signature (Node.js)
const crypto = require('crypto');

function verifySignature(rawBody, signatureHeader, secret) {
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  const received = String(signatureHeader).replace(/^sha256=/, '');
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received));
}
Verify HMAC signature (Python)
import hmac, hashlib

def verify_signature(raw_body: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    received = signature.removeprefix("sha256=")
    return hmac.compare_digest(expected, received)

Billing and credits

App billing summary
curl "https://api.shernova.com/v1/apps/YOUR_APP_ID/billing" \
  -H "Authorization: Bearer YOUR_DASHBOARD_JWT" \
  -H "X-Shernova-Organization-Id: YOUR_ORG_ID"
Ledger entries
const res = await fetch(
  `${process.env.SHERNOVA_API}/v1/apps/${appId}/billing/ledger?limit=20`,
  {
    headers: {
      Authorization: `Bearer ${jwt}`,
      'X-Shernova-Organization-Id': orgId,
    },
  },
);
console.log(await res.json());