Server-Side Integration
Integrate biometric age verification into your server-side application using direct API calls.
Prerequisites
- Secret key (
sk_...) and publishable key (pk_...) from your Dashboard - A server capable of making HTTPS requests (Node.js, PHP, Python, etc.)
For API key authentication details and request/response schemas, see the Sessions API Reference.
Quick Start
Server-side integration is straightforward:
- Create a session - Call the API with your secret key
- Redirect users - Send them to the
verifyUrlreturned in the response - Validate results - When users return, call the validate endpoint with the
sessionId
No JavaScript SDK required, no state parameters to sign.
Choose Your Pattern
- JSON pattern (SPA/native):
GET/POST /api/verify-agereturns{ verifyUrl, sessionId }; the client navigates toverifyUrl; later call/api/verify-completeto validate. - Redirect pattern (server-rendered):
POST /verify-age->res.redirect(verifyUrl);/verifiedroute validatessessionIdand redirects to success/failure pages. - Tip: When testing directly in a browser, use the redirect pattern so you can click a button and follow the flow.
See Redirect Behavior for the parameters appended to your returnUrl.
Environment Setup
Store your secret key in an environment variable and load it at startup:
- Node.js
- PHP
# .env
SAFEPASSAGE_SECRET_KEY=sk_...
// server.js (at top)
require('dotenv').config(); // or: import 'dotenv/config'
# .env
SAFEPASSAGE_SECRET_KEY=sk_...
<?php
// If you're using vlucas/phpdotenv:
// (new Dotenv\Dotenv(__DIR__))->load();
$secretKey = getenv('SAFEPASSAGE_SECRET_KEY');
Your secret key is used for authentication. The merchantId field is optional; if you include it, it must match the authenticated key.
Server-Side Flow
1. Create Session
- Node.js
- PHP
app.get('/api/verify-age', async (req, res) => {
const secretKey = process.env.SAFEPASSAGE_SECRET_KEY;
const response = await fetch('https://api.safepassageapp.com/api/v1/sessions/create', {
method: 'POST',
headers: {
'Authorization': `Bearer ${secretKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
returnUrl: 'https://yoursite.com/verified',
externalUserId: req.query.user
})
});
if (!response.ok) {
return res.status(response.status).json({ error: 'Session creation failed' });
}
const session = await response.json();
res.json({ verifyUrl: session.verifyUrl, sessionId: session.sessionId });
});
<?php
$secretKey = getenv('SAFEPASSAGE_SECRET_KEY');
$payload = json_encode([
'returnUrl' => 'https://yoursite.com/verified',
'externalUserId' => $_GET['user'] ?? null,
]);
$ch = curl_init('https://api.safepassageapp.com/api/v1/sessions/create');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $secretKey,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => $payload,
]);
$rawResponse = curl_exec($ch);
$statusCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($statusCode < 200 || $statusCode >= 300) {
http_response_code($statusCode ?: 500);
echo json_encode(['error' => 'Session creation failed']);
exit;
}
$session = json_decode($rawResponse, true);
echo json_encode([
'verifyUrl' => $session['verifyUrl'] ?? null,
'sessionId' => $session['sessionId'] ?? null,
]);
To redirect immediately instead of returning JSON, use res.redirect(session.verifyUrl) in Node.js or header('Location: ' . $session['verifyUrl']); exit; in PHP.
If you want to open verification in a new tab or popup window from the client:
// Full browser tab
window.open(session.verifyUrl, '_blank');
// Fixed-size popup window
window.open(session.verifyUrl, 'verification', 'width=600,height=700');
When cancelUrl is provided during session creation, closing the verification window in a new-tab flow will attempt to redirect the opener to cancelUrl with status=cancelled and sessionId. You can also listen for the safepassage:verification:complete PostMessage to detect status=cancelled explicitly.
2. Validate Results
When users return to your returnUrl, validate the sessionId server-side:
- Node.js
- PHP
app.get('/verified', async (req, res) => {
const { sessionId } = req.query;
const response = await fetch('https://api.safepassageapp.com/api/v1/sessions/validate', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.SAFEPASSAGE_SECRET_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ sessionId })
});
const result = await response.json();
if (result.accessGranted) {
// Grant access
}
});
<?php
$sessionId = $_GET['sessionId'] ?? null;
$ch = curl_init('https://api.safepassageapp.com/api/v1/sessions/validate');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('SAFEPASSAGE_SECRET_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['sessionId' => $sessionId]),
]);
$rawResponse = curl_exec($ch);
$statusCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($statusCode < 200 || $statusCode >= 300) {
http_response_code($statusCode ?: 500);
echo 'Validation failed';
exit;
}
$result = json_decode($rawResponse, true);
if (!empty($result['accessGranted'])) {
// Grant access
}
The validation response includes externalUserId when provided at session creation, so you can map results back to your user record.
Complete Working Examples
Need a full working sample? Contact support@safepassageapp.com and we can share ready-to-run examples for your stack.
Webhooks
Configure webhooks in your dashboard to get real-time notifications:
- Node.js
- PHP
app.post('/webhooks/safepassage', async (req, res) => {
const { event, data } = req.body;
if (event === 'verification.completed' && data.externalUserId) {
// Update your user record
await updateUser(data.externalUserId, {
ageVerified: data.verified,
verifiedAt: new Date()
});
}
res.json({ received: true });
});
<?php
$rawBody = file_get_contents('php://input');
$payload = json_decode($rawBody, true);
$event = $payload['event'] ?? null;
$data = $payload['data'] ?? [];
if ($event === 'verification.completed' && !empty($data['externalUserId'])) {
// Update your user record for this user.
// Example: update_user($data['externalUserId'], [...]);
}
http_response_code(200);
header('Content-Type: application/json');
echo json_encode(['received' => true]);
See Webhooks documentation for details.
Query Parameters
Append query parameters to verifyUrl to customize the verification UI:
| Parameter | Values | Effect |
|---|---|---|
lang | en, de, es, fr, pt | Override the UI language. Optional - most integrations should omit this and let the browser language apply. |
skip_intro | true | Skip intro screen |
auto_return | true | Skip success screen, redirect immediately |
- Node.js
- PHP
const url = new URL(session.verifyUrl);
url.searchParams.set('lang', 'es'); // Spanish UI
url.searchParams.set('skip_intro', 'true');
url.searchParams.set('auto_return', 'true');
res.redirect(url.toString());
<?php
$verifyUrl = $session['verifyUrl'];
$parts = parse_url($verifyUrl);
parse_str($parts['query'] ?? '', $query);
$query['lang'] = 'es';
$query['skip_intro'] = 'true';
$query['auto_return'] = 'true';
$rebuiltUrl =
($parts['scheme'] ?? 'https') . '://' .
($parts['host'] ?? '') .
($parts['path'] ?? '') .
'?' . http_build_query($query);
header('Location: ' . $rebuiltUrl);
exit;
Locale variants are normalized automatically (e.g. pt-BR -> pt, de-DE -> de). Unsupported lang values are ignored; the UI falls through to the user's browser language, then English. See Sessions API - Query Parameters for full details.
WordPress Integrations
If your site runs on WordPress, follow the WordPress Integration Guide for wp_remote_post(), REST routes, shortcodes, and content-gating examples.
Troubleshooting
| Issue | Solution |
|---|---|
merchantId must match the authenticated API key | Ensure merchantId matches the Authorization header key, or omit merchantId entirely |
| Verification page not loading | Use verifyUrl exactly as returned - don't modify or reconstruct it |
| Validation returns 401 | Use secret key (sk_...) for validation |
| Session expired | Sessions expire after 10 minutes |
| Session already used | Sessions can only be validated once |
| Domain validation error | Add your domain to Dashboard -> Settings |
For proxy/iframe embedding, pass the full verifyUrl unchanged.
Support: support@safepassageapp.com