Sessions API
API reference for creating and validating age verification sessions.
This page is an API reference. For step-by-step tutorials with complete working examples, see the Server Integration Guide.
Authentication
Authorization: Bearer YOUR_API_KEY
| Key Type | Prefix | Capabilities |
|---|---|---|
| Publishable | pk_... | Create sessions (SDK or direct API) |
| Secret | sk_... | Create sessions, validate sessions |
For server-side integrations, authenticate with your secret key (sk_...) in the Authorization header (or x-api-key). The merchantId field is optional; if provided, it must match the authenticated API key.
Base URL
https://api.safepassageapp.com/api/v1
Endpoints
Create Session
Creates a new verification session.
POST /sessions/create
Request Body
{
"returnUrl": "https://example.com/verified",
"cancelUrl": "https://example.com/closed",
"merchantName": "Example Co",
"externalUserId": "user_12345",
"verificationMode": "L1",
"challengeAge": 25
}
| Field | Required | Description |
|---|---|---|
merchantId | No | Optional; if provided, must match the API key used for authentication |
returnUrl | Yes | Where to redirect after verification |
cancelUrl | No | Where to redirect if the user closes or abandons verification (new-tab flows) |
merchantName | No | Display name shown to users during verification |
externalUserId | No | Your identifier for this user. If your user record doesn't exist yet, generate a UUID and associate it with the user later. Appears in webhooks, dashboard, and API responses for correlation. |
verificationMode | No | "L1" (face estimation) or "L2" (always ID) |
challengeAge | No | Minimum age threshold (25-99) |
Response
{
"sessionId": "550e8400-e29b-41d4-a716-446655440000",
"sessionToken": "v2.eyJzZXNzaW9uSWQiOiIuLi4ifQ....",
"verifyUrl": "https://av.safepassageapp.com/?sessionId=...&sessionToken=...",
"expiresAt": "2024-01-20T10:10:00Z",
"testMode": false,
"sandboxMode": false,
"externalUserId": "user_123",
"handoffToken": "..."
}
| Field | Type | Description |
|---|---|---|
sessionId | string | Session identifier for validation |
sessionToken | string | Opaque signed token used by the verification UI (not a JWT). Do not parse or send to validate. |
verifyUrl | string | Full verification URL (use as-is) |
expiresAt | string | ISO 8601 timestamp |
testMode | boolean | Whether the session is in test mode (deprecated; always false) |
sandboxMode | boolean | Whether sandbox mode is active for this session |
externalUserId | string | null | Your user identifier (if provided). Returns null when not provided. |
handoffToken | string | QR handoff token for mobile/QR flows |
billingBlock | object | Optional gating details when verification is blocked |
Redirect users to verifyUrl. Use sessionId for validation.
verifyUrl contents: verifyUrl is an opaque, fully-formed URL. It always includes sessionId and sessionToken, and may include returnUrl, cancelUrl, merchantName, externalUserId, blocked, and portalUrl. Use it exactly as returned — do not reconstruct or remove query parameters.
If billingBlock is present:
code:SUBSCRIPTION_REQUIRED,PLAN_LIMIT_REACHED, orSANDBOX_LIMIT_REACHEDmessage: user-facing explanationportalUrl: billing portal URL for upgrades (optional)
HTTP Status Codes
| Status | Description |
|---|---|
201 Created | Session created successfully |
400 Bad Request | Invalid request body or parameters |
401 Unauthorized | Invalid or missing API key |
403 Forbidden | Insufficient permissions |
Notes
- Sessions expire after 10 minutes
verificationModeandchallengeAgeoverrides are accepted with both publishable and secret keys. For enforcement, prefer server-side session creation.- Domain validation is performed on
returnUrl - Rate limit: 300 requests per minute per account (see Rate Limits)
merchantIdis optional for authenticated server-side requests; if provided, it must match the authenticated API key
Redirect Behavior
SafePassage redirects users back to your returnUrl after verification completes. The following query parameters are appended:
| Parameter | Always Present | Description |
|---|---|---|
sessionId | Yes | Session identifier for validation |
status | Yes | Verification outcome (see values below) |
timestamp | Yes | Milliseconds since epoch |
externalUserId | No | Only if provided at session creation |
sessionToken | No | Internal use - do not send to validate endpoint |
Status Values
| Status | Description |
|---|---|
verified | User passed age verification |
failed | Verification failed |
cancelled | User cancelled or abandoned verification before completion |
Note: cancelled is emitted when a user closes or abandons a new-tab verification flow. Redirect and webhook flows include this status when a cancel is detected.
If you provided externalUserId at session creation, it will be echoed in redirect parameters. For server-side integrations, treat /sessions/validate as the source of truth for externalUserId.
Always validate sessionId server-side before granting access.
Validate Session
Validates a completed session. Requires a secret key.
POST /sessions/validate
Request Body
{
"sessionId": "550e8400-e29b-41d4-a716-446655440000"
}
Only send sessionId. Do not include sessionToken.
Response
{
"sessionId": "550e8400-e29b-41d4-a716-446655440000",
"merchantId": "5da36244-8cc2-43bf-bf20-1219f8da4d00",
"verified": true,
"accessGranted": true,
"status": "verified",
"verificationMode": "L1",
"challengeAge": 25,
"externalUserId": "user_12345",
"testMode": false,
"timestamp": "2024-01-20T10:00:00Z",
"expiresAt": "2024-01-20T10:10:00Z"
}
| Field | Type | Description |
|---|---|---|
sessionId | string | Session identifier |
verified | boolean | true if verification passed — use this for access decisions |
accessGranted | boolean | Server-derived access decision (verified in current policy) |
merchantId | string | Tenant identifier for the session |
status | string | "verified", "failed", "cancelled", or "pending" |
verificationMode | string | "L1" or "L2" |
challengeAge | number | The age threshold used |
externalUserId | string | null | Your user identifier (if provided). Returns null when not provided. |
testMode | boolean | Whether the session is in test mode (deprecated; always false) |
timestamp | string | ISO 8601 timestamp for verification completion |
expiresAt | string | ISO 8601 session expiration timestamp |
Notes
- Sessions can only be validated once
- Requires private API key
- Rate limit: 100 requests per minute per account (see Rate Limits)
estimatedAgeis deprecated and not returned
Error Responses
Errors return the following format:
{
"statusCode": 400,
"message": ["property sessionToken should not exist"],
"timestamp": "2025-01-20T10:35:56.545Z",
"path": "/api/v1/sessions/validate"
}
Example when authorization is missing:
{
"statusCode": 401,
"message": "API key required",
"timestamp": "2026-01-27T18:23:36.326Z",
"path": "/api/v1/sessions/create",
"error": "Unauthorized"
}
Notes:
messagecan be a string or an array of strings (validation errors may include multiple messages)timestampis ISO 8601 formatpathindicates which endpoint returned the error
Common Errors
| Status | Example message | When it happens |
|---|---|---|
401 Unauthorized | Private API key required for session validation | Using a pk_... key to validate |
403 Forbidden | Session does not belong to this tenant | Validating a session from a different tenant |
404 Not Found | Session not found | The sessionId is unknown |
400 Bad Request | Session has expired | The session expired before validation |
400 Bad Request | Session has already been used | The session was already validated |
400 Bad Request | Return URL domain not allowed | Creating a session with a returnUrl domain not on your allowlist |
429 Too Many Requests | (rate limit) | Exceeding rate limits |
Rate Limits
All API rate limits are enforced per account (tenant). All API keys under the same account share a single rate-limit bucket per endpoint.
| Endpoint | Default Limit | Window |
|---|---|---|
POST /sessions/create | 300 requests | 60 seconds |
POST /sessions/validate | 100 requests | 60 seconds |
When a rate limit is exceeded, the API returns 429 Too Many Requests. Every response includes headers to help you track your usage:
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests allowed in the window |
X-RateLimit-Window | Window duration in seconds |
X-RateLimit-Remaining | Requests remaining in the current window |
X-RateLimit-Retry-After | Seconds until the window resets (only on 429) |
If your integration requires higher throughput, contact our sales team to set up an enterprise account. Limits can be increased on a per-tenant basis.
Code Examples
Create Session (curl)
curl -X POST "https://api.safepassageapp.com/api/v1/sessions/create" \
-H "Authorization: Bearer sk_your_secret_key" \
-H "Content-Type: application/json" \
-d '{
"returnUrl": "https://example.com/verified",
"externalUserId": "user_12345",
"verificationMode": "L1",
"challengeAge": 25
}'
Validate Session (curl)
curl -X POST "https://api.safepassageapp.com/api/v1/sessions/validate" \
-H "Authorization: Bearer sk_your_secret_key" \
-H "Content-Type: application/json" \
-d '{
"sessionId": "550e8400-e29b-41d4-a716-446655440000"
}'
Related
- Server Integration Guide — Step-by-step tutorial with Express and PHP examples
- Webhooks API — Real-time event notifications
- JavaScript SDK — Client-side integration (no server required)