Add the Vonage Verify API to the Backend
Using the Vonage Server SDK
Vonage exposes a standard HTTP API under the hood. That means, in theory, you could integrate Verify by sending raw HTTP requests yourself (for example with fetch, axios, etc.).
So why use the Vonage Node SDK?
Using the SDK helps because:
Authentication is easier and safer: Verify uses JWT-based auth with a private key. The SDK handles the signing flow correctly, so you’re less likely to make mistakes.
Cleaner code: instead of manually building URLs, headers, and parsing response formats, you call methods like
newRequest()andcheckCode().Better maintenance: when Vonage updates the API or adds features, the SDK is usually updated to match.
Fewer “gotchas”: things like request formatting and expected fields are handled consistently.
Let’s add the SDK to our app.js file:
require("dotenv").config();
const fs = require("fs");
const express = require("express");
const cors = require("cors");
const { Auth } = require("@vonage/auth");
const { Verify2 } = require("@vonage/verify2");
const app = express();
const port = process.env.PORT || 3000;
app.use(cors());
app.use(express.json());
// Create Vonage credentials (JWT auth)
const credentials = new Auth({
applicationId: process.env.VONAGE_APPLICATION_ID,
privateKey: process.env.VONAGE_PRIVATE_KEY_PATH,
});
// Verify client (Verify API v2)
const verifyClient = new Verify2(credentials);
// Health check endpoint
app.get('/health', (req, res) => {
res.json({ status: 'ok' });
});
// Run the server
app.listen(port, () => {
console.log(`Backend listening on port ${port}`);
});
What’s happening here?
- The backend needs to prove to Vonage: “I’m allowed to call this API.”
- Vonage uses JWT (JSON Web Tokens) signed by your private key for that proof.
- The SDK generates and attaches the JWT automatically whenever it calls Vonage.
Start Verification: POST /verification
This endpoint starts the verification process. The mobile app calls your backend with a phone number. Your backend then asks Vonage to start a verification request.
What happens in this endpoint?
- The user enters their phone number in the mobile app.
- The mobile app sends the phone number to your backend.
- Your backend starts a Verify request:
- first tries Silent Authentication
- if that can’t be completed, falls back to SMS
app.post("/verification", async (req, res) => {
const { phone } = req.body || {};
if (!phone) {
return res.status(400).json({ error: "Phone number is required." });
}
try {
const result = await verifyClient.newRequest({
brand: "DemoApp",
workflow: [
{ channel: "silent_auth", to: phone },
{ channel: "sms", to: phone },
],
});
return res.json({
request_id: result.requestId,
check_url: result.checkUrl,
});
} catch (error) {
const status = error?.response?.status || 500;
const details = error?.response?.data || error?.message;
console.error("Vonage Verify newRequest failed:", details);
return res.status(status).json({
error: "Failed to start verification",
details: typeof details === "string" ? details : undefined,
});
}
});
Understanding request_id and check_url:
request_id: a unique identifier for this verification attempt. Think of it like a “receipt number” for the verification.check_url: used for Silent Authentication. Your backend returns this URL to the mobile app. The mobile app calls it to prove “this request is coming from the phone number’s mobile network”.
Check Verification Code: POST /check-code
If Silent Auth fails or isn’t available, Vonage will fall back to SMS and the user will receive a code. The mobile app sends the code to your backend along with the request_id.
app.post("/check-code", async (req, res) => {
const { request_id, code } = req.body || {};
if (!request_id || !code) {
return res.status(400).json({ error: "request_id and code are required." });
}
try {
const status = await verifyClient.checkCode(request_id, code);
return res.json({
verified: status === "completed",
status,
});
} catch (error) {
const status = error?.response?.status || 400;
const details = error?.response?.data || error?.message;
return res.status(status).json({
error: "Failed to check code",
details: typeof details === "string" ? details : undefined,
});
}
});
Callbacks
A callback (also called a webhook) is a URL in your backend that an external service (Vonage) can call to notify you about events.
Instead of your backend constantly asking Vonage: “Has Silent Auth finished yet? What about now? Now?”
Vonage can push the result to you: “Silent Auth finished. Here is the final status.”
That push notification is the callback.
Why are callbacks useful here? Silent Authentication may take time and can complete asynchronously. Using a callback means:
- Your backend doesn’t have to poll Vonage repeatedly
- You get a definitive event when the verification changes state
- It scales better in real systems
To configure the callback URL in the Dashboard, open the Vonage Dashboard:
- Go to Applications
- Select your application → Edit
- Find Network Registry
- Enable Verify (SA)
- Set the callback URL (where your server listens), for example:
https://your-domain.com/callback
Note: If you’re running locally, Vonage can’t reach http://localhost:3000. You’ll need a public URL (commonly a tunnel like ngrok).
To implement the callback, add a new method to your Express application like so:
app.post("/callback", (req, res) => {
console.log("Callback received:", req.body);
return res.status(200).json({ ok: true });
});
For now, we log the event so you can see what Vonage sends. We’ll extend it in the next section to store state and make the mobile app react to those updates.
Add an In-Memory State
A verification flow is not “one request and done”. It has a lifecycle:
- started
- pending (silent auth / sms)
- completed or failed/expired
If you don’t store state anywhere, your backend has no memory of what happened, and:
/callbackcan only log data (not very useful)- The app can’t reliably know the current status
- Debugging becomes painful (“it worked once, then it didn’t…”)
A store gives you a single source of truth.
In production, you would use a database (e.g. Postgres/Redis), but for the tutorial, we can just use a Map.
Step 1: Create the store with a Map
In Node.js, a Map is an easy way to store key/value pairs in memory.
Add this near the top of your app.js:
// In-memory store for tutorial purposes:
// request_id -> verification state
const verificationStore = new Map();
Add a helper function for validating required request body fields. Add it just below the verificationStore declaration:
function requireFields(obj, fields) {
for (const f of fields) {
if (!obj || obj[f] == null || obj[f] === "") return f;
}
return null;
}
This returns the name of the first missing field, or null if all fields are present.
Each entry will be keyed by request_id.
A typical entry might look like:
Step 2: Save initial state when creating a verification
When you call verifyClient.newRequest(...) in /verification, you receive a request_id.
That is the perfect key to store the initial state.
Inside your /verification endpoint, right after you get result:
verificationStore.set(result.requestId, {
phone,
status: "started",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
Now the backend “remembers” that a verification has started.
Step 3: Update state when the callback receives events
A callback (webhook) is Vonage telling your backend: “Something changed. Here’s the new status.”
Instead of only logging the payload, we update the stored state:
app.post("/callback", (req, res) => {
const { request_id, status } = req.body || {};
if (!request_id) return res.status(400).json({ error: "Missing request_id" });
const current = verificationStore.get(request_id) || {
phone: null,
status: "unknown",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
lastEvent: null,
};
const updated = {
...current,
status: status || current.status,
updatedAt: new Date().toISOString(),
lastEvent: req.body,
};
verificationStore.set(request_id, updated);
return res.status(200).json({ ok: true });
});
Webhook deliveries can be retried, meaning you might receive the same event multiple times. Updating the store like this is naturally idempotent: setting the same status again doesn’t break anything.
Step 4: Add a status endpoint for the mobile app
Now we can provide a simple endpoint the app can call to check the current state:
app.get("/status/:request_id", (req, res) => {
const { request_id } = req.params;
const entry = verificationStore.get(request_id);
if (!entry) return res.status(404).json({ error: "Unknown request_id" });
return res.json({
request_id,
status: entry.status,
updated_at: entry.updatedAt,
});
});
This is especially useful for Silent Auth because the app can poll every 1–2 seconds for a short period instead of waiting blindly.
Step 5: Add POST /next
The /next endpoint tells Vonage to skip the current workflow channel and move to the next one. In our case, that means skipping Silent Auth and sending an SMS immediately.
This is useful in the Android app when the Silent Auth request fails (bad network, SDK error, etc.) — instead of waiting ~20 seconds for Vonage to time out naturally, the app calls /next and the user gets an SMS right away.
app.post("/next", async (req, res) => {
try {
const missing = requireFields(req.body, ["requestId"]);
if (missing) {
return res.status(400).json({ error: `Field '${missing}' is required.` });
}
const { requestId } = req.body;
const entry = verificationStore.get(requestId);
if (!entry) {
return res.status(404).json({ error: "Unknown request_id" });
}
console.log("Moving to next workflow (SMS) for:", requestId);
// Call Vonage to move to next workflow
const result = await verifyClient.nextWorkflow(requestId);
console.log("Vonage nextWorkflow result:", result);
// Update last event
const updated = {
...entry,
updatedAt: new Date().toISOString(),
lastEvent: { source: "next_workflow", result },
};
verificationStore.set(requestId, updated);
return res.status(200).json({ ok: true });
} catch (error) {
const status = error?.response?.status || 500;
const details = error?.response?.data || error?.message;
console.error("Error /next:", details);
return res.status(status).json({
error: "Failed to move workflow",
details: typeof details === "string" ? details : undefined,
});
}
});
Note: If /next fails, it's not fatal. Vonage will automatically fall back to SMS after the Silent Auth timeout. The Android app should show the SMS input screen regardless of whether this call succeeds.
Getting Started with Silent Authentication
Silent Authentication takes quite a bit to understand. This tutorial shows you how to build an integration from scratch with Nodejs and Kotlin