Skip to main content

Sessions API

API reference for creating and validating age verification sessions.

Looking for implementation guidance?

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 TypePrefixCapabilities
Publishablepk_...Create sessions (SDK or direct API)
Secretsk_...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
}
FieldRequiredDescription
merchantIdNoOptional; if provided, must match the API key used for authentication
returnUrlYesWhere to redirect after verification
cancelUrlNoWhere to redirect if the user closes or abandons verification (new-tab flows)
merchantNameNoDisplay name shown to users during verification
externalUserIdNoYour 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.
verificationModeNo"L1" (face estimation) or "L2" (always ID)
challengeAgeNoMinimum 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": "..."
}
FieldTypeDescription
sessionIdstringSession identifier for validation
sessionTokenstringOpaque signed token used by the verification UI (not a JWT). Do not parse or send to validate.
verifyUrlstringFull verification URL (use as-is)
expiresAtstringISO 8601 timestamp
testModebooleanWhether the session is in test mode (deprecated; always false)
sandboxModebooleanWhether sandbox mode is active for this session
externalUserIdstring | nullYour user identifier (if provided). Returns null when not provided.
handoffTokenstringQR handoff token for mobile/QR flows
billingBlockobjectOptional 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, or SANDBOX_LIMIT_REACHED
  • message: user-facing explanation
  • portalUrl: billing portal URL for upgrades (optional)

HTTP Status Codes

StatusDescription
201 CreatedSession created successfully
400 Bad RequestInvalid request body or parameters
401 UnauthorizedInvalid or missing API key
403 ForbiddenInsufficient permissions

Notes

  • Sessions expire after 10 minutes
  • verificationMode and challengeAge overrides 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)
  • merchantId is 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:

ParameterAlways PresentDescription
sessionIdYesSession identifier for validation
statusYesVerification outcome (see values below)
timestampYesMilliseconds since epoch
externalUserIdNoOnly if provided at session creation
sessionTokenNoInternal use - do not send to validate endpoint

Status Values

StatusDescription
verifiedUser passed age verification
failedVerification failed
cancelledUser 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"
}
FieldTypeDescription
sessionIdstringSession identifier
verifiedbooleantrue if verification passed — use this for access decisions
accessGrantedbooleanServer-derived access decision (verified in current policy)
merchantIdstringTenant identifier for the session
statusstring"verified", "failed", "cancelled", or "pending"
verificationModestring"L1" or "L2"
challengeAgenumberThe age threshold used
externalUserIdstring | nullYour user identifier (if provided). Returns null when not provided.
testModebooleanWhether the session is in test mode (deprecated; always false)
timestampstringISO 8601 timestamp for verification completion
expiresAtstringISO 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)
  • estimatedAge is 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:

  • message can be a string or an array of strings (validation errors may include multiple messages)
  • timestamp is ISO 8601 format
  • path indicates which endpoint returned the error

Common Errors

StatusExample messageWhen it happens
401 UnauthorizedPrivate API key required for session validationUsing a pk_... key to validate
403 ForbiddenSession does not belong to this tenantValidating a session from a different tenant
404 Not FoundSession not foundThe sessionId is unknown
400 Bad RequestSession has expiredThe session expired before validation
400 Bad RequestSession has already been usedThe session was already validated
400 Bad RequestReturn URL domain not allowedCreating 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.

EndpointDefault LimitWindow
POST /sessions/create300 requests60 seconds
POST /sessions/validate100 requests60 seconds

When a rate limit is exceeded, the API returns 429 Too Many Requests. Every response includes headers to help you track your usage:

HeaderDescription
X-RateLimit-LimitMaximum requests allowed in the window
X-RateLimit-WindowWindow duration in seconds
X-RateLimit-RemainingRequests remaining in the current window
X-RateLimit-Retry-AfterSeconds until the window resets (only on 429)
Need higher limits?

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"
}'