npm package: @safepassage/sdk
Description: SafePassage SDK - Lightweight redirect-based age verification
Author: SafePassage
Latest version: 3.5.2

# SafePassage SDK v3.5.2

A lightweight SDK for integrating SafePassage age verification into your website or application.

## Features

- **Ultra-lightweight**: ~18KB minified
- **Simple integration**: 5 lines of code to get started
- **Two modes**: Same-tab redirect or new-tab popup
- **TypeScript support**: Full type definitions included
- **Auto-environment detection**: Works seamlessly across environments
- **Secure**: HMAC-signed state parameters, automatic session management
- **Compliant**: Enforces minimum age of 25

## Changelog

### 3.5.2
- SafePassage version alignment release to match PrivateAV at `3.5.2`
- No SDK API changes from `3.5.0`

### 3.5.0
- Added `language` option for UI localization (`en`, `de`, `es`, `fr`, `pt`)
- Per-verification `language` override via `verify({ language: 'es' })`

### 3.4.11
- Fixed CDN documentation URLs (files are at package root, not `/dist/`)

### 3.4.9
- Prevents `onCancel` from firing after a successful new-tab verification when the popup closes.

## Installation

```bash
npm install @safepassage/sdk
```

Or load directly from jsDelivr CDN (no bundler required):

```html
<script src="https://cdn.jsdelivr.net/npm/@safepassage/sdk@latest/safepassage.min.js"></script>
```

## Quick Start

### With npm/bundler

```javascript
import { SafePassage } from '@safepassage/sdk';

const sp = new SafePassage({
  apiKey: 'pk_...',  // Your public key from the dashboard
  returnUrl: window.location.origin + '/verified'
});

// Start verification - redirects user to SafePassage
await sp.verify();
```

### With CDN (no bundler)

```html
<script src="https://cdn.jsdelivr.net/npm/@safepassage/sdk@latest/safepassage.min.js"></script>
<script>
  const sp = new SafePassage({
    apiKey: 'pk_...',
    returnUrl: window.location.origin + '/verified'
  });

  document.getElementById('verify-btn').onclick = () => sp.verify();
</script>
```

That's it! The SDK handles session creation automatically.

## Configuration

| Option | Type | Required | Description |
|--------|------|----------|-------------|
| apiKey | string | Yes | Your public API key (`pk_...`) |
| returnUrl | string | Yes | URL to redirect after verification |
| cancelUrl | string | No | URL to redirect to if user closes the verification window (new-tab mode) |
| environment | string | No | `'production'` or `'staging'` (auto-detected) |
| mode | string | No | `'redirect'` (default) or `'new-tab'` |
| defaultChallengeAge | number | No | Default minimum age (25 or higher) |
| defaultVerificationMode | string | No | `'L1'` or `'L2'` |
| onComplete | function | No | Callback for new-tab mode |
| onCancel | function | No | Called when user closes popup (new-tab mode). Return `false` to suppress automatic `cancelUrl` redirect. |
| language | string | No | Default UI language (`'en'`, `'de'`, `'es'`, `'fr'`, `'pt'`). Can be overridden per verification. |
| onError | function | No | Error handler |

## Verification Options

Override settings per-verification:

```javascript
await sp.verify({
  challengeAge: 30,           // Override minimum age for this session
  verificationMode: 'L2',     // Force ID verification for this session
  externalUserId: 'user-123', // Your user ID (returned in webhooks)
  skipIntro: true,            // Skip intro screen
  autoReturn: true,           // Auto-redirect after success
  language: 'es'              // Override UI language for this session
});
```

### Verification Modes

- **L1**: Age estimation using computer vision (faster, less friction)
- **L2**: Full ID document verification (more thorough)

## Integration Modes

### Same-Tab Redirect (Default)

User is redirected to SafePassage, then back to your `returnUrl`:

```javascript
const sp = new SafePassage({
  apiKey: 'pk_...',
  returnUrl: '/age-verified'
});

await sp.verify();
// User is redirected to SafePassage...
// After verification, user returns to /age-verified?sessionId=xxx&status=verified
```

### New-Tab Mode

Verification opens in a popup window:

```javascript
const sp = new SafePassage({
  apiKey: 'pk_...',
  returnUrl: '/age-verified',
  cancelUrl: '/age-cancelled',
  mode: 'new-tab',
  onComplete: (result) => {
    console.log('Verification complete:', result.sessionId, result.status);
    // Validate on your server, then update UI
  },
  onCancel: () => {
    console.log('User closed the verification window');
    // Return false if you want to handle navigation manually
    // return false;
  },
  onError: (error) => {
    console.error('Verification error:', error.message);
  }
});

await sp.verify();
```

If `cancelUrl` is provided, the SDK will redirect the opener to `cancelUrl` with
`status=cancelled` and `sessionId` when the user closes the verification window.
Return `false` from `onCancel` to suppress the automatic redirect.

## Server-Side Validation (Required!)

After verification completes, **always validate the result on your server** before granting access:

```javascript
// Node.js / Express example
app.get('/age-verified', async (req, res) => {
  const { sessionId } = req.query;

  // Validate with your SECRET key (sk_...)
  const response = await fetch(
    `https://api.safepassageapp.com/api/v1/sessions/${sessionId}`,
    {
      headers: {
        'Authorization': `Bearer ${process.env.SAFEPASSAGE_SECRET_KEY}`
      }
    }
  );

  const session = await response.json();

  if (session.status === 'VERIFIED') {
    // Grant access
    req.session.ageVerified = true;
    res.redirect('/content');
  } else {
    res.redirect('/age-verification-failed');
  }
});
```

> **Security Note**: Never trust client-side verification status alone. Always validate server-side using your secret key.

## Webhooks (Recommended)

For reliable verification tracking, configure webhooks in your dashboard:

```javascript
// Webhook payload example
{
  "event": "verification.completed",
  "sessionId": "abc-123",
  "verified": true,
  "externalUserId": "your-user-id",  // If provided during verify()
  "timestamp": "2025-01-15T10:30:00Z"
}
```

## Complete Example

### HTML + CDN

```html
<!DOCTYPE html>
<html>
<head>
  <title>Age Verification</title>
  <script src="https://cdn.jsdelivr.net/npm/@safepassage/sdk@latest/safepassage.min.js"></script>
</head>
<body>
  <button id="verify-btn">Verify Your Age</button>

  <script>
    const sp = new SafePassage({
      apiKey: 'pk_...',
      returnUrl: window.location.origin + '/verified'
    });

    document.getElementById('verify-btn').onclick = () => sp.verify();
  </script>
</body>
</html>
```

### React Component

```jsx
import { useState } from 'react';
import { SafePassage } from '@safepassage/sdk';

function AgeGate() {
  const [verifying, setVerifying] = useState(false);

  const sp = new SafePassage({
    apiKey: process.env.NEXT_PUBLIC_SAFEPASSAGE_KEY,
    returnUrl: window.location.origin + '/verified'
  });

  const handleVerify = async () => {
    setVerifying(true);
    try {
      await sp.verify();
    } catch (error) {
      console.error('Failed to start verification:', error);
      setVerifying(false);
    }
  };

  return (
    <button onClick={handleVerify} disabled={verifying}>
      {verifying ? 'Redirecting...' : 'Verify Your Age'}
    </button>
  );
}
```

## TypeScript

Full TypeScript support included:

```typescript
import { SafePassage, SafePassageConfig, VerificationResult } from '@safepassage/sdk';

const config: SafePassageConfig = {
  apiKey: process.env.NEXT_PUBLIC_SAFEPASSAGE_KEY!,
  returnUrl: '/verified',
  mode: 'new-tab',
  onComplete: (result: VerificationResult) => {
    console.log(`Session ${result.sessionId}: ${result.status}`);
  }
};

const sp = new SafePassage(config);
```

## Skip Parameters

For streamlined embedded flows:

```javascript
// Skip intro screen (go directly to camera)
await sp.verify({ skipIntro: true });

// Auto-redirect after success (no success screen)
await sp.verify({ autoReturn: true });

// Both - minimal user interaction
await sp.verify({ skipIntro: true, autoReturn: true });
```

## Error Handling

```javascript
const sp = new SafePassage({
  apiKey: 'pk_...',
  returnUrl: '/verified',
  onError: (error) => {
    if (error.message.includes('popup')) {
      alert('Please allow popups for age verification');
    } else if (error.message.includes('rate limit')) {
      alert('Too many attempts. Please wait a moment.');
    } else {
      console.error('Verification error:', error);
    }
  }
});
```

## API Keys

SafePassage uses two types of API keys:

| Key Type | Prefix | Use Case |
|----------|--------|----------|
| Public Key | `pk_` | Client-side SDK (this package) |
| Secret Key | `sk_` | Server-side validation only |

> **Important**: This SDK only works with public keys (`pk_`). For server-side integrations using secret keys, use the [Direct API](https://docs.safepassageapp.com/api) instead.

## Browser Support

- Chrome 60+
- Firefox 60+
- Safari 12+
- Edge 79+
- Mobile browsers (iOS Safari, Chrome for Android)

## Security Best Practices

1. **Use public keys client-side** - Never expose secret keys in browser code
2. **Validate server-side** - Always verify results using your secret key
3. **Configure webhooks** - For reliable, tamper-proof verification notifications
4. **Register callback URLs** - Pre-register your `returnUrl` in the dashboard
5. **Use HTTPS** - SDK enforces HTTPS in production

## Cleanup

When done with the SDK (e.g., in SPA route changes):

```javascript
sp.destroy();
```

## Migration from v3.0.x

If you were using merchant-generated session IDs:

```javascript
// Old (v3.0.x)
sp.verify({ sessionId: crypto.randomUUID() });

// New (v3.4+) - sessionId is auto-generated
await sp.verify();
```

The SDK now creates sessions automatically via the API when using public keys.

## Support

- [Documentation](https://docs.safepassageapp.com)
- [API Reference](https://docs.safepassageapp.com/api)
- [Dashboard](https://portal.safepassageapp.com)