Documentation
DOCUMENTATION

VerifiedCore API Reference

Developer APIs for eSIM data, voice calling and a prepaid wallet — one API key, official SDKs for Node.js, Python, PHP and Go, and a sandbox that needs no backend.

A plain REST API. Official SDKs for Node.js, Python, PHP and Go — or call it directly from any language. Base URL: https://api.verifiedcore.com/api/v1

Open live API reference (Swagger UI) ↗

The guide below is hand-written for onboarding. The live reference is generated directly from the running API — every endpoint, parameter, and schema reflects what's actually deployed right now.

Option A
Use an official SDK

Best fit: you're on Node.js, Python, PHP or Go and want typed methods and automatic retries instead of hand-writing HTTP calls.

  • ✓ eSIM, call plans and wallet as typed resources
  • ✓ Built-in retry on transient network errors
  • ✓ Same method names across all four languages
Option B
Call the REST API directly

Best fit: you're on Go, Ruby, Java, C#, or any language/framework without an official SDK — or you simply prefer working against plain HTTP. Every example below (including eSIM, Call Plans, and Wallet) is REST-only and copy-paste ready in cURL.

  • ✓ Works from any language with an HTTP client — nothing to install
  • ✓ Same endpoints and responses the SDKs use
  • ✓ Full live reference generated from the running API (Swagger, linked above)

Quickstart

Make your first real call in 3 steps: install (or skip straight to a request), initialize, then list live eSIM plans and provision one for a user. Every code block has a tab per language — pick yours.

1. Install

npm install @verixo/sdk

2. Initialize

import Verixo from '@verixo/sdk'
const vc = new Verixo(process.env.VC_API_KEY)

3. List plans + provision an eSIM

// List live eSIM plans — Nigeria, 7-day validity
const packages = await vc.esim.listPackages('NG', 7)
const pkg = packages[0]

// Provision one for your user — returns an install QR
const esim = await vc.esim.purchase({
  packageId: pkg.packageId,
  countryCode: 'NG',
  priceUsd: pkg.priceUsd,
})
console.log(esim.qrCodeUrl)
💰

Wondering what it costs? Listing plans and the sandbox are free; you only pay for what you provision, from a prepaid wallet. No credit card required to start. See full pricing →

Authentication

Authenticated requests send your API key directly in the Authorization header --no Bearer prefix. Get your key from the dashboard after signing up.

# All authenticated requests require this header
Authorization: vc_live_your_api_key_here

API keys are prefixed with vc_live_ for production and vc_test_ for sandbox. Sandbox keys use mock data and never charge your wallet.

Creating an API key

Sign up and log in to get a JWT, then mint an API key with it — keys are scoped and named so you can issue separate ones per environment or integration.

Scopes are enforced: verify, esim:read, esim:purchase, calls, wallet:read, webhooks:manage, automation, analytics:read. Omit scopes to get all of them. A call outside a key's scopes returns 403 INSUFFICIENT_SCOPE; team, key management and admin actions are dashboard-only.

Optionally lock a key to your servers with an IP allowlist (IPs or CIDR ranges, set when creating the key or later in the dashboard, or via PUT /auth/api-keys/{keyId}/allowed-ips). Calls from any other address return 403 IP_NOT_ALLOWED.

curl -X POST "https://api.verifiedcore.com/api/v1/auth/api-keys" \
  -H "Authorization: Bearer <your JWT>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Production backend",
    "scopes": ["verify", "esim:read"],
    "environment": "live"
  }'

# Response
{
  "key": "vc_live_8f2a91c4b7e6...",
  "meta": { "id": "key_abc123", "name": "Production backend", "environment": "live", "createdAt": "2026-06-22T10:00:00Z" }
}
⚠️

The raw key is shown once, at creation. Store it in a secrets manager or encrypted env var, never in client-side code or version control — anyone holding a vc_live_ key can debit your wallet.

Manage existing keys with GET /auth/api-keys (list), DELETE /auth/api-keys/{keyId} (revoke one), or DELETE /auth/api-keys (revoke all).

Verify API

Send one-time codes to your own users for sign-up, login and 2FA, then check what they typed. The SMS carries your brand ("Acme Bank: your verification code is 482913…"), is delivered through Infobip with automatic Twilio failover, and is billed to your wallet per code sent. Codes are never stored — only a keyed hash.

🧪

Test mode: with a vc_test_ key nothing is sent or charged, and the code is always 123456.

🚦

Current status: Verify is open for integration in test mode. Live SMS is being enabled per network; until then, live keys return 503 LIVE_SENDING_DISABLED.

POST /verify/start

to must be an E.164 mobile number. Pass your end user's ip to enable per-IP abuse limits. Starting again for the same number cancels the previous code.

const res = await fetch("https://api.verifiedcore.com/api/v1/verify/start", {
  method: "POST",
  headers: { Authorization: process.env.VC_API_KEY, "Content-Type": "application/json" },
  body: JSON.stringify({ to: "+2348031234567", ip: req.ip }),
})
const verification = await res.json()   // keep verification.id for the check step

POST /verify/check

Send the id from start (or the to number) and the code. valid is true only for a correct, unexpired code. Each code allows 5 attempts and expires after 5 minutes by default.

const res = await fetch("https://api.verifiedcore.com/api/v1/verify/check", {
  method: "POST",
  headers: { Authorization: process.env.VC_API_KEY, "Content-Type": "application/json" },
  body: JSON.stringify({ id: verification.id, code: userInput }),
})
const result = await res.json()
if (result.valid) { /* sign the user in */ }

Other endpoints

GET  /api/v1/verify/{id}            # one verification
GET  /api/v1/verify?page=0&size=25   # recent verifications
GET  /api/v1/verify/settings         # brand name, allowed countries, code length, expiry
PUT  /api/v1/verify/settings         # {"brandName":"Acme Bank","allowedCountries":["NG","GH"],"codeLength":6,"ttlSeconds":300}

Errors & fraud protection

Errors return {"error":{"code","message"}}. Built-in protection you should know about:

  • BUSINESS_VERIFICATION_REQUIRED (403) — live codes need a business-verified account; submit your CAC details under Dashboard → Business. Test keys are unaffected.
  • COUNTRY_NOT_ALLOWED (403) — only countries in your settings are served (default NG, GH, KE, ZA).
  • COUNTRY_REQUIRES_APPROVAL (403) — the destination is outside the self-serve list; request it and we'll review.
  • UNSUPPORTED_NUMBER_TYPE / INVALID_NUMBER (400) — premium-rate and non-mobile numbers are refused.
  • RESEND_TOO_SOON / TOO_MANY_REQUESTS (429) — 30-second resend cooldown and per-number, per-IP and per-account limits.
  • VERIFY_PAUSED (429) — sending pauses automatically when many codes go out but very few are verified (SMS pumping).
  • INSUFFICIENT_BALANCE (402), DELIVERY_FAILED (502 — you are not charged).

Subscribe a webhook to verification.approved and verification.failed to react without polling.

Webhooks

Register an HTTPS endpoint and we POST events to it as they happen: esim.provisioned, wallet.credited, wallet.low_balance, callplan.subscribed and special.package.purchased. Every request carries an X-VerifiedCore-Signature: sha256=… header — an HMAC-SHA256 of the raw body using your endpoint's signing secret.

# Register a webhook
curl -X POST "https://api.verifiedcore.com/api/v1/webhooks" \
  -H "Authorization: $VC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://yourapp.com/hooks/verifiedcore","events":["esim.provisioned","wallet.low_balance"]}'

# List your webhooks
curl "https://api.verifiedcore.com/api/v1/webhooks" -H "Authorization: $VC_API_KEY"

# Send a test event
curl -X POST "https://api.verifiedcore.com/api/v1/webhooks/{id}/test" -H "Authorization: $VC_API_KEY"

# Remove one
curl -X DELETE "https://api.verifiedcore.com/api/v1/webhooks/{id}" -H "Authorization: $VC_API_KEY"
🔁

Delivery rules: endpoints must be public https:// URLs. Reply with any 2xx within 5 seconds; we retry twice (after 2s and 10s) on timeouts, 5xx, 408 and 429, and don't follow redirects. The same event can arrive more than once — dedupe on its id.

There's no update endpoint yet — to change a webhook's URL or event list, delete it and create a new one.

// esim.provisioned
{
  "id": "evt_3f2c9a…",
  "event": "esim.provisioned",
  "createdAt": "2026-09-23T12:00:04Z",
  "data": {
    "profileId": "…",
    "iccid": "8901…",
    "qrCodeUrl": "https://…",
    "activationCode": "LPA:1$…",
    "countryCode": "NG",
    "dataGb": 1,
    "validityDays": 7,
    "priceUsd": 3.85
  }
}

// Verify the signature (Node.js)
import crypto from 'crypto'
const expected = 'sha256=' + crypto.createHmac('sha256', SIGNING_SECRET).update(rawBody).digest('hex')
const valid = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(req.headers['x-verifiedcore-signature']))

eSIM API

Provision real eSIM data packages worldwide — available as esim in every SDK, or call the REST endpoints below directly.

GET /esim/packages

Public — browse available data packages for a country. No API key required.

// GET /esim/packages?countryCode=NG
{
  "packages": [
    { "packageId": "esim_ng_1gb_7d",  "dataGb": 1, "days": 7,  "priceUsd": 6.99,  "networks": "Airtel [4G]" },
    { "packageId": "esim_ng_3gb_30d", "dataGb": 3, "days": 30, "priceUsd": 18.99, "networks": "Airtel [4G]" },
    { "packageId": "esim_ng_5gb_30d", "dataGb": 5, "days": 30, "priceUsd": 28.99, "networks": "Airtel [4G]" }
  ]
}

POST /esim/purchase

Provisions a real eSIM via our carrier partner and debits your wallet. Returns everything needed to activate the SIM — a QR code, a one-tap iOS 17.4+ install link, and manual SM-DP+ details as a fallback.

curl -X POST "https://api.verifiedcore.com/api/v1/esim/purchase" \
  -H "Authorization: $VC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"packageId":"esim_ng_1gb_7d","countryCode":"NG"}'

# Response
{
  "profileId": "esim_a1b2c3",
  "iccid": "8901000000000000001",
  "activationCode": "LPA:1$smdp.example.com$matching-id-here",
  "qrCodeUrl": "https://esim.airalo.com/qr/abc123",
  "directAppleInstallUrl": "https://esimsetup.apple.com/esim_qrcode_provisioning?carddata=...",
  "smdpAddress": "smdp.example.com",
  "matchingId": "matching-id-here",
  "apnType": "automatic",
  "apnValue": "internet",
  "isRoaming": false,
  "carrierName": "Airtel",
  "dataGbIncluded": 1,
  "validityDays": 7,
  "countryCode": "NG",
  "status": "ACTIVE"
}

GET /esim/{iccid}/usage

Check remaining data balance for a provisioned eSIM. Rate limited to 10 requests/min per ICCID.

curl "https://api.verifiedcore.com/api/v1/esim/8901000000000000001/usage" \
  -H "Authorization: $VC_API_KEY"

GET /esim/my

List every eSIM you've provisioned, with current status and remaining data.

curl "https://api.verifiedcore.com/api/v1/esim/my" -H "Authorization: $VC_API_KEY"

Call Plans API

Subscription-based outbound calling minutes. Three fixed tiers — not yet wrapped by the SDKs, REST only.

GET /call-plans

Public — list the available tiers.

// GET /call-plans
[
  { "id": "basic",    "tier": "BASIC",    "name": "Basic Plan",    "minutes": 50,  "priceUsd": 4.99,  "perMin": 0.10,  "popular": false,
    "features": ["50 outbound minutes", "Incoming free", "10 countries", "Standard quality"] },
  { "id": "standard", "tier": "STANDARD", "name": "Standard Plan", "minutes": 150, "priceUsd": 9.99,  "perMin": 0.067, "popular": true,
    "features": ["150 outbound minutes", "Incoming free", "40 countries", "HD voice", "Call recording"] },
  { "id": "premium",  "tier": "PREMIUM",  "name": "Premium Plan",  "minutes": -1,  "priceUsd": 19.99, "perMin": 0,
    "popular": false, "features": ["Unlimited outbound", "Incoming free", "80+ countries", "Ultra-HD voice", "Recording + transcription", "Priority routing"] }
]

POST /call-plans/subscribe

curl -X POST "https://api.verifiedcore.com/api/v1/call-plans/subscribe" \
  -H "Authorization: $VC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"tier":"STANDARD","paymentProvider":"STRIPE","paymentReference":"pi_abc123"}'

Manage your subscription

GET /call-plans/my — active subscription (204 if none) · DELETE /call-plans/my — cancel · GET /call-plans/history — past subscriptions

Looking for one-off bundles instead of a recurring plan? See GET /special-packages and POST /special-packages/purchase — same auth model, separate product (starter/business/developer/regional bundles mixing SMS, minutes, and eSIM data).

Wallet & Billing

Every paid call — number purchase, eSIM provision, call plan subscription — debits a prepaid wallet balance. Top up via the Payments API (currency-routed: NGN → OPay, GHS → Flutterwave, everything else → Stripe).

# Current balance
curl "https://api.verifiedcore.com/api/v1/wallet" -H "Authorization: $VC_API_KEY"

# Transaction history
curl "https://api.verifiedcore.com/api/v1/wallet/transactions" -H "Authorization: $VC_API_KEY"

To fund your wallet programmatically, see POST /payment/initiate and GET /payment/currencies in the live API reference — full payment-provider documentation lives there since it covers bank transfers, virtual accounts, and exchange rates in addition to card payments.

Email API

Send transactional email through our infrastructure — useful if you're already calling our API for OTP delivery and don't want a second provider just for receipts or notifications. REST only, not yet wrapped by any SDK.

POST /email/send

curl -X POST "https://api.verifiedcore.com/api/v1/email/send" \
  -H "Authorization: $VC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "user@example.com",
    "subject": "Your order is confirmed",
    "body": "Thanks for your purchase — your order #1234 is on its way."
  }'

# Response
{ "messageId": "msg_abc123" }

to, subject, and body are required. from is optional and defaults to our sending domain.

Legacy

The Numbers, Sessions, WebSocket and delivery-analytics endpoints below belong to the retired OTP number-rental product. They're documented for existing integrations only — don't build new products on them. To verify your own users (sign-up, login, 2FA), use the Verify API.

Numbers API

GET /numbers/search

Returns virtual numbers ranked by Health Score™ for a given service and country.

ParamTypeDescription
serviceSlugstringRequired. Service identifier, e.g. whatsapp, google, telegram
countryCodestringISO 3166-1 alpha-2. e.g. NG, US, DE
minScorenumberMinimum Health Score (0–100). Default: 0
limitnumberMax results (1–50). Default: 10
const { numbers } = await vc.numbers.search({
  serviceSlug: 'whatsapp',
  countryCode: 'NG',
  minScore: 80,
  limit: 5,
})

Sessions API

A session is created when you purchase a number. The 90-second SLA clock starts at session creation.

⏱️

90-second guaranteed delivery SLA. If the OTP isn't delivered within 90 seconds, your wallet is refunded automatically — no support ticket, no manual review. This is enforced server-side by the same watchdog regardless of whether you're using an SDK or calling the REST API directly.

POST /numbers/purchase

// Request
{ "numberId": "num_abc123" }

// Response
{
  "sessionToken": "sess_xyz789",
  "expiresAt": "2026-05-21T12:00:90Z",
  "price": 0.15,
  "currency": "USD"
}

GET /sessions/{token}

// Response
{
  "sessionToken": "sess_xyz789",
  "status": "DELIVERED",   // PENDING | DELIVERED | EXPIRED | REFUNDED
  "otp": "847291",
  "deliveredAt": "2026-05-21T12:00:04Z",
  "latencyMs": 4200,
  "healthScore": 94
}

WebSocket push

Connect to wss://api.verifiedcore.com/ws and subscribe to your session token via STOMP. OTPs are pushed in real time — no polling needed.

// Purchase picks the best available number server-side --
// there's no numberId param, since a candidate from search()
// may already be gone by the time you'd reference it back.
const session = await vc.numbers.purchase({
  serviceSlug: 'whatsapp',
  countryCode: 'NG',
})

vc.subscribe(session.sessionToken, ({ otp, latencyMs }) => {
  console.log(`OTP: ${otp} in ${latencyMs}ms`)
})
💡

The Go, Ruby, Java, and C# examples above poll GET /sessions/{token} every 2 seconds for simplicity — that's enough for most integrations given the 90-second SLA window. For true real-time push without polling, connect any STOMP-over-WebSocket client available for your language to the same wss://api.verifiedcore.com/ws endpoint and subscribe to /topic/otp/{sessionToken} — the Node.js, Python, and PHP SDKs do exactly this internally.

Analytics

GET /analytics/rates returns 30-day rolling delivery rates per service and country.

curl "https://api.verifiedcore.com/api/v1/analytics/rates?serviceSlug=whatsapp" \
  -H "Authorization: $VC_API_KEY"

SDKs

Node.js, Python, and PHP SDKs are published and live today, covering Numbers, Sessions, Analytics, and real-time OTP push via subscribe(). Native Go, Ruby, and Java packages are in development — until they ship, the above already has working REST examples in Go, Ruby, Java, and C#, alongside cURL. eSIM, Call Plans, and Wallet aren't wrapped by any SDK yet either — those sections are REST-only for every language for now.

Framework examples

Copy-paste recipes for the most common frameworks. Each example lists eSIM plans and provisions one using the official SDK where available, or plain HTTP where not.

// pubspec.yaml — add dio
// dependencies:
//   dio: ^5.4.0
//
// Call the API from YOUR backend, not the app — never ship an API key in a
// mobile binary. This service talks to your own server, which proxies to
// VerifiedCore.

import 'package:dio/dio.dart';

class ESimService {
  final _dio = Dio(BaseOptions(baseUrl: 'https://your-backend.example.com'));

  Future<List<dynamic>> plans(String countryCode) async {
    final res = await _dio.get('/esim/plans', queryParameters: {'country': countryCode});
    return res.data as List<dynamic>;
  }

  Future<String> buy(String packageId) async {
    final res = await _dio.post('/esim/buy', data: {'packageId': packageId});
    return res.data['qrCodeUrl'] as String; // show with qr_flutter
  }
}
📦

Don't see your framework? Every example above maps directly to the REST examples — any language with an HTTP client works identically.

Error reference

CodeStatusDescription
AUTH_001401Missing or invalid API key.
AUTH_002401JWT token expired.
NUMBER_001404Number ID not found or already reserved.
NUMBER_002422Health score below your requested minScore.
WALLET_001402Insufficient wallet balance.
SESSION_001404Session token not found or expired.
SLA_001408OTP not delivered within 90s — auto-refunded.
RATE_001429Rate limit exceeded. See Retry-After header.

Rate limits

⏳

Default: 5 requests/second per API key, burst up to 20. Enterprise plans have custom limits. Rate-limited requests return HTTP 429 with a Retry-After header — applies identically whether you're on an SDK or raw REST.

Changelog

2026-09-23
Docs — eSIM-first quickstart, corrected base URL

Base URL corrected to https://api.verifiedcore.com/api/v1. Quickstart now starts with eSIM; OTP number-rental endpoints moved to Legacy.

2026-05-15
v1.4 — Privacy mode GA

Privacy mode (zero OTP storage) is now generally available. EU IPs get privacy mode by default.

2026-04-01
v1.3 — eSIM API

eSIM data packages API launched. Nigeria, Ghana, Kenya, South Africa, US, UK, EU, India, Australia. 1 GB packages from $2.99.

2026-03-10
v1.2 — Voice OTP

Voice OTP via Telnyx now available. Useful for users who can't receive SMS.