# Bespot Gatekeeper Web SDK — Integration Guide
This guide explains how to add the Gatekeeper Web SDK to your web application. You do **not** need Node.js, npm, or a build step. You download a pre-built package, host two JavaScript files, configure four settings, pass a JWT, and call the SDK API.
## Table of Contents
1. [Overview](#1-overview)
2. [Bespot prerequisites](#2-bespot-prerequisites)
3. [SDK package installation](#3-sdk-package-installation)
4. [Distribution format selection](#4-distribution-format-selection)
5. [Runtime configuration](#5-runtime-configuration)
6. [Integration sequence](#6-integration-sequence)
7. [Integration examples](#7-integration-examples)
8. [API reference](#8-api-reference)
9. [Error handling](#9-error-handling)
10. [Geolocation](#10-geolocation)
11. [Access token rotation](#11-access-token-rotation)
12. [Common integration mistakes](#12-common-integration-mistakes)
13. [Troubleshooting](#13-troubleshooting)
14. [Security](#14-security)
15. [SDK version upgrades](#15-sdk-version-upgrades)
16. [Support](#16-support)
---
## 1. Overview
The Gatekeeper Web SDK runs in the user's browser. It:
1. **Registers** the browser as a device (once, on first successful `initialize`).
2. **Runs checks** against the Gatekeeper backend and returns an `action` and `ticket` you can use in your application logic.
Typical flow:
```text
Your page loads SDK → you pass config + JWT → initialize() → check() → you read action/ticket
```
The SDK is a **single class** named `SafeSDK`. It is a **singleton**: every `new SafeSDK()` in the same page returns the same instance.
### What the SDK does **not** do
| Misconception | Reality |
|---------------|---------|
| "I set credentials in a `.env` file and the bundle reads them." | **Wrong.** The JavaScript bundle contains **no** API key, application ID, or backend URL. You must supply them in your page or a config script at runtime. |
| "I can put `client_secret` in frontend code to get a JWT." | **Wrong.** OAuth secrets belong on **your server** only. The browser receives a short-lived JWT from your backend. |
| "`check()` throws when something fails." | **Wrong.** `check()` **returns** an `Error` object on failure. Only `initialize()` **throws**. |
| "I can call `check()` before `initialize()`." | **Wrong.** `check()` returns `NotInitialized` until `initialize()` completes successfully. |
---
## 2. Bespot prerequisites
Before writing code, sign up at **[gatekeeper.bespot.com](https://gatekeeper.bespot.com)** to create your account and obtain credentials.
| What | URL / value | Purpose |
|------|-------------|---------|
| Account portal | [gatekeeper.bespot.com](https://gatekeeper.bespot.com) | Sign in and manage your Bespot account |
| Gatekeeper API (`baseUrl`) | `bespot-gatekeeper-base-url` (e.g. `https://gatekeeper.bespotcompany.com`) | API host Bespot assigns when you register — set this in SDK runtime config |
| Product documentation | [docs.bespot.com](https://docs.bespot.com) | Official Bespot guides and API reference |
Do **not** use the account portal URL as `baseUrl`. The SDK must call your assigned Gatekeeper API host, not the sign-in site.
Collect these values:
| Name | Used as | Example | Where it goes |
|------|---------|---------|---------------|
| **API key** | `apiKey` | `your-api-key` (e.g. `13CTrcYiya9NNnRyd3jXA21CULPPDSqM90sdFnGs`) | Runtime config (browser) |
| **Application ID** | `applicationId` | `your-app-id` (e.g. `mywebapp.mycompany.com`) | Runtime config (browser) |
| **Application version** | `applicationVersion` | `your-app-version` (e.g. `2.4.1`) | Runtime config (browser) — **your app release**, not the SDK tarball version |
| **Gatekeeper base URL** | `baseUrl` | `bespot-gatekeeper-base-url` (e.g. `https://gatekeeper.bespotcompany.com`) | Runtime config (browser)|
| **JWT access token** | argument to `initialize()` | `eyJhbGciOi...` | Fetched at runtime from **your backend** |
### Backend-only credentials (for JWT issuance — not in the browser)
Your server uses these to call the auth issuer. The browser never sees `client_secret`.
| Name | Purpose |
|------|---------|
| `AUTH_SERVER_URL` | Auth issuer base URL (e.g. `https://gatekeeper.auth.eu-west-1.amazoncognito.com`) |
| `CLIENT_ID` | OAuth client id |
| `CLIENT_SECRET` | OAuth client secret — **server only** |
| `SCOPE` | OAuth scope (e.g. `main_antifraud_resource_server/public`) |
The **API key** (`apiKey` in SDK config) is sent on **Gatekeeper** requests (`x-api-key` header). It is separate from the OAuth token exchange unless your auth setup explicitly requires otherwise.
### Environment requirements
- **HTTPS** in production (required for geolocation and device persistence).
- **CORS**: the Gatekeeper `baseUrl` must accept browser requests from your site's origin. Allowlisting is configured by Bespot. If DevTools shows a CORS error on requests to `baseUrl`, see [§13](#cors-errors-in-the-browser).
- **Geolocation**: the SDK needs browser location permission to obtain the user's position. Tell your users why location is required; if they deny permission, location-dependent checks will not work (see [§10](#10-geolocation)).
---
## 3. SDK package installation
### Option A — npm (recommended for module bundlers)
```bash
npm install @bespot/gatekeeper-web-sdk
```
```js
import SafeSDK from '@bespot/gatekeeper-web-sdk'
```
### Option B — CDN / self-hosted (no build step)
Download the latest versioned bundles from [GitHub Releases](https://github.com/bespot/gatekeeper-web-sdk-release/releases):
- `safe-sdk.esm.min.js` — ES module format
- `safe-sdk.umd.min.js` — UMD format (classic script tags)
Copy the downloaded file(s) to your CDN or web server, for example:
```text
https://your-cdn.example.com/sdk/safe-sdk.esm.min.js
https://your-cdn.example.com/sdk/safe-sdk.umd.min.js
```
The filenames are always `safe-sdk.esm.min.js` and `safe-sdk.umd.min.js`.
---
## 4. Distribution format selection
You need **one** of the two files, not both on the same page (unless you are testing).
| File | Choose when | How you load it |
|------|-------------|-----------------|
| `safe-sdk.esm.min.js` | Your app uses ES modules (`import`), or a bundler (Vite, Webpack, React, Vue, Angular) | `
```
If you use Option B with ESM, still set `globalThis.__SAFE_SDK_CONFIG__` in an earlier classic script, then `import SafeSDK` and `new SafeSDK()`.
### Important notes
- **Do not commit** real API keys or tokens to git. Generate `runtime-config.js` in your deployment pipeline or serve it from a server that injects environment values.
- Calling `new SafeSDK(newConfig)` again on the same page **updates** config on the existing singleton instance.
---
## 6. Integration sequence
Follow this sequence **every time** you integrate:
| Step | Action | Required? | On failure |
|------|--------|-----------|------------|
| 1 | Create `SafeSDK` with runtime config | Yes | **Throws** `InvalidSDKConfiguration` |
| 2 | `await sdk.initialize(jwt)` | Yes | **Throws** (e.g. `InvalidAccessToken`, `NetworkError`, `AuthenticationFailed`) |
| 3 | `sdk.setUserId(id)` | No | — |
| 4 | `await sdk.check()` | When you need a result | **Returns** success object or `Error` (does not throw) |
| 5 | `await sdk.subscribe()` | No | Interval from [server configuration](#periodic-checks) |
| 6 | `sdk.unsubscribe()` | When stopping periodic checks | — |
```text
new SafeSDK(config)
↓
await initialize(jwt) ← must succeed before any check
↓
setUserId(...) ← optional
↓
await check() ← one-time check
↓
await subscribe() ← optional
↓
unsubscribe() ← when done
```
---
## 7. Integration examples
Replace placeholder URLs and credentials with your real values. Copy-ready files: [../templates/](../templates/).
### ESM example (`safe-sdk.esm.min.js`)
```html
Gatekeeper SDK — ESM
```
### UMD example (`safe-sdk.umd.min.js`)
```html
Gatekeeper SDK — UMD
```
---
## 8. API reference
### Constructor
```ts
new SafeSDK(config?: {
baseUrl: string
apiKey: string
applicationId: string
applicationVersion: string
})
```
All four fields are required (non-empty strings) whether passed here or via `globalThis.__SAFE_SDK_CONFIG__`.
### Methods
| Method | Throws? | Description |
|--------|---------|-------------|
| `initialize(accessToken: string): Promise` | **Yes** | Validates JWT shape, registers device with Gatekeeper. **Must succeed before `check`.** |
| `setAccessToken(accessToken: string): void` | **Yes** | Updates JWT **without** re-registering. Use after JWT refresh. Does **not** replace `initialize`. |
| `setUserId(userId: string): void` | No | Sets your customer/client related unique user identifier |
| `check()` | No | Runs one Gatekeeper check. Returns a [check result](integration-guide.md#check-result-shape) object or an `Error` subclass (does not throw). |
| `subscribe(): Promise` | No | Starts periodic checks on the [registration interval](#periodic-checks). Pauses while browser tab is hidden. |
| `unsubscribe(): void` | No | Stops periodic checks and clears timers. |
### Properties
| Member | Description |
|--------|-------------|
| `applicationId` | The `applicationId` you configured |
| `applicationVersion` | The `applicationVersion` you configured |
| `userId` | Current user id from `setUserId`, or `''` |
| `lastCheckResult` | Last `check()` outcome — [check result](#check-result-shape) object or `Error` subclass, or `undefined` before the first check |
### Check result shape
When `check()` succeeds, it returns a plain object (not an `Error`):
| Field | Type | Description |
|-------|------|-------------|
| `action` | `string` | Gatekeeper decision for your application logic (allowed values depend on your Bespot configuration) |
| `ticket` | `string` | Opaque ticket for this check — use per your integration agreement with Bespot |
| `timestamp` | `number` | Unix time in **milliseconds** when the result was recorded client-side |
```js
const result = await sdk.check()
if (!(result instanceof Error)) {
console.log(result.action, result.ticket, result.timestamp)
}
```
### Periodic checks
`subscribe()` runs `check()` on a fixed interval. The interval is **not** passed as a JavaScript argument.
The interval is set by Bespot in `configuration.periodic_interval` from device registration (string or number, in **milliseconds**).
Call `subscribe()` with **no arguments** after a successful `initialize()`. Read `sdk.lastCheckResult` after each periodic run.
---
## 9. Error handling
There are **two different** error behaviors. Mixing them up is the most common integration bug.
### Rule 1: `initialize` and `setAccessToken` **throw**
Always wrap in `try/catch`:
```js
try {
await sdk.initialize(jwt)
} catch (error) {
console.error('Initialize failed:', error.name, error.message)
}
```
### Rule 2: `check` **returns** an error — it does **not** throw
Always use `instanceof Error`:
```js
const result = await sdk.check()
if (result instanceof Error) {
console.error('Check failed:', result.name, result.message)
} else {
console.log('Check succeeded:', result.action, result.ticket)
}
```
### Error reference
See [error-reference.md](error-reference.md) for the full catalog.
When logging errors, use **`error.name`** and **`error.message`**.
| `error.name` | Meaning | Typical fix |
|--------------|---------|-------------|
| `InvalidSDKConfiguration` | Missing or empty `baseUrl`, `apiKey`, `applicationId`, or `applicationVersion` | Set all four config fields before `new SafeSDK()` |
| `InvalidAccessToken` | JWT is empty or only whitespace | Pass a non-empty token to `initialize` |
| `InvalidAccessTokenFormat` | JWT is not `xxx.yyy.zzz` (three non-empty parts) | Fix token format from your auth server |
| `NotInitialized` | `check()` called before successful `initialize`, or only `setAccessToken` was called | Call `await initialize(jwt)` first |
| `InvalidApiKey` | Gatekeeper rejected the API key | Verify `apiKey` with Bespot |
| `AuthenticationFailed` | HTTP 401 from Gatekeeper | JWT expired or invalid — refresh token ([§11](#11-access-token-rotation)) |
| `AuthorizationFailed` | HTTP 403 from Gatekeeper | JWT or app lacks permission |
| `GeolocationNotSupported` | Browser has no Geolocation API | Use a supported browser; cannot recover on that client |
| `NoRecipeFound` | No backend recipe for your app config | Contact Bespot with `applicationId` + `applicationVersion` |
| `NoCheckFound` | No checks available for your app | Contact Bespot with `applicationId` + `applicationVersion` |
| `NetworkError` | Browser could not reach Gatekeeper (`fetch` failed) | Check network, `baseUrl`, HTTPS; if DevTools shows CORS, contact Bespot ([§13](#cors-errors-in-the-browser)) |
| `ServerError` | Gatekeeper returned HTTP 5xx | Retry later; contact Bespot if persistent |
| `InvalidResponseError` | Response body was missing required fields | Contact Bespot if persistent |
| `StorageUnavailable` | Browser storage could not be used (cookies, localStorage, etc.) | Retry in a normal browser session; ask the user to allow site data if prompted |
| `UnknownError` | Unclassified failure | Log `error.message`; contact support |
---
## 10. Geolocation
The SDK uses the browser **Geolocation API** to obtain the user's location during checks. The browser will prompt the user for permission when location is requested.
**Your responsibility:** Explain to your users why location access is needed — before or when your app runs Gatekeeper checks. Clear messaging helps users make an informed choice.
**If the user denies permission:** Checks that depend on user location will not work. Plan your UX, permission flow, and fallback behavior accordingly.
**Requirements:** Production sites must be served over **HTTPS** (browsers block geolocation on non-secure origins).
---
## 11. Access token rotation
JWTs expire. After the **first** successful `initialize`, refresh the token with `setAccessToken` — **do not** call `initialize` again just to rotate the JWT.
```js
// After initialize() succeeded once:
sdk.setAccessToken(freshJwtFromYourBackend)
```
| Method | Registers device again? | When to use |
|--------|-------------------------|-------------|
| `initialize(jwt)` | **Yes** (first time in this browser profile) | Once at session start |
| `setAccessToken(jwt)` | **No** | Every time you get a new JWT from your backend |
### Proactive refresh (recommended)
```js
// When your auth layer knows the token will expire soon:
const fresh = await fetch('/api/gatekeeper-token').then((r) => r.text())
sdk.setAccessToken(fresh.trim())
```
### Reactive refresh (if a check fails with AuthenticationFailed)
```js
const result = await sdk.check()
if (result instanceof Error && result.name === 'AuthenticationFailed') {
const fresh = await fetch('/api/gatekeeper-token').then((r) => r.text())
sdk.setAccessToken(fresh.trim())
await sdk.check() // optional immediate retry
}
```
**Note:** If a `check()` is already in flight when you call `setAccessToken`, that request keeps the old token. The new token applies to the **next** call.
---
## 12. Common integration mistakes
| Mistake | What happens | What to do instead |
|---------|--------------|-------------------|
| Calling `check()` before `await initialize()` | `NotInitialized` | Always `initialize` first |
| Using `try/catch` around `check()` only | Missed failures | Use `if (result instanceof Error)` |
| Passing arguments to `subscribe()` | Not supported | Call `subscribe()` with no arguments; interval comes from [server registration](#periodic-checks) |
| Using SDK tarball version as `applicationVersion` | `NoRecipeFound` or auth errors | Use the app version registered with Bespot |
| Trailing slash on `baseUrl` | May cause bad URLs | Use `https://gatekeeper.bespotcompany.com` not `https://gatekeeper.bespotcompany.com/` |
| Putting `client_secret` in frontend | Security risk | Token exchange on your server only |
| Calling `initialize()` on every JWT refresh | Unnecessary re-registration | Use `setAccessToken()` after the first `initialize()` |
| Loading UMD script before `__SAFE_SDK_CONFIG__` | `InvalidSDKConfiguration` | Config script must run **first** |
| Expecting `.env` in the tarball to configure production | Nothing happens | Set runtime config in your host page or `runtime-config.js` |
---
## 13. Troubleshooting
### `initialize` throws immediately
1. JWT is a non-empty string with exactly two dots (three segments).
2. All four config fields are set and non-empty.
3. JWT is not expired (get a fresh one from your backend).
4. Browser can reach `baseUrl` (open DevTools → Network tab).
5. CORS allows your site's origin on Gatekeeper responses — if not, see [CORS errors](#cors-errors-in-the-browser).
### CORS errors in the browser
CORS (Cross-Origin Resource Sharing) is enforced by the browser. The SDK calls your Gatekeeper `baseUrl` directly from the user's browser, so that host must respond with headers that allow **your site's origin** (for example `https://mywebapp.mycompany.com`).
**Typical symptoms**
- DevTools → Console: messages such as `blocked by CORS policy`, `No 'Access-Control-Allow-Origin' header`, or failed **preflight** (`OPTIONS`) requests.
- DevTools → Network: Gatekeeper requests marked as failed before a response body is returned; the SDK may surface `NetworkError` on `initialize` or `check`.
**Gatekeeper `baseUrl` (contact Bespot)**
You cannot allowlist your origin in JavaScript or by changing SDK config. Bespot must enable CORS for your production (and staging) origins on the Gatekeeper environment tied to your `baseUrl`.
If the failing request URL matches your `baseUrl` (e.g. `https://gatekeeper.bespotcompany.com/...`), contact **Bespot** and include:
- Your site's **origin** as shown in the browser (scheme + host + port, e.g. `https://shop.example.com`)
- Your `applicationId` and `applicationVersion`
- The exact CORS message from DevTools (screenshot or copy/paste)
- Whether the failure happens on `initialize`, `check`, or both
### `check` returns `InvalidApiKey` or `AuthenticationFailed`
1. Confirm `apiKey`, `applicationId`, and `applicationVersion` match Bespot's records.
2. Confirm JWT was issued for the correct resource/scope.
3. If token may be expired, refresh with `setAccessToken`.
### `check` returns `NetworkError`
1. User is offline or behind a blocking proxy.
2. `baseUrl` is wrong or unreachable from the user's network.
3. Mixed content: HTTPS page calling HTTP API (blocked by browser).
### No periodic check results appear
1. `subscribe()` must be called **after** `initialize()` succeeds (wrap `initialize` in `try/catch` and only subscribe in the success path).
2. Confirm you did not call `unsubscribe()` earlier on the same page.
### Periodic checks seem to stop or slow down
1. The SDK **pauses** the timer while the tab is **hidden** and resumes when visible — this is intentional.
2. Confirm you did not call `unsubscribe()`.
### `StorageUnavailable` on `initialize`
Browser storage (cookies, localStorage, etc.) could not be used — for example in private mode or with strict tracking protection.
1. Ask the user to allow site data or retry in a normal browser window.
2. Retry `initialize()` in a standard (non-private) session.
---
## 14. Security
- **Never** commit `.env` files, API keys, JWTs, or `client_secret` to source control.
- **Never** run OAuth client-credentials with `client_secret` in browser code.
- **Never** expose long-lived secrets in frontend code.
- Issue **short-lived** JWTs from your backend; rotate with `setAccessToken`.
- **Rotate credentials** (API keys, client secrets) on a regular schedule per your security policy.
- Store production secrets in a **secret manager** or CI/CD vault; inject into `runtime-config.js` or your backend at deploy time — not in the static SDK bundle.
- Serve SDK files and config over **HTTPS** in all environments.
- The API key is sent from the browser (`x-api-key` header). Treat it as a client-scoped credential with permissions defined by Bespot.
- Do not log full JWTs in analytics or error reporting.
---
## 15. SDK version upgrades
To upgrade the SDK:
**npm:** `npm install @bespot/gatekeeper-web-sdk@latest`
**CDN / self-hosted:**
1. Download the newer bundles from [GitHub Releases](https://github.com/bespot/gatekeeper-web-sdk-release/releases).
2. Replace the hosted `safe-sdk.esm.min.js` and/or `safe-sdk.umd.min.js` files.
After upgrading either way:
1. Re-test `initialize` → `check` → `subscribe` in staging.
2. Read [CHANGELOG](../CHANGELOG.md) for that release before deploying to production.
Upgrading the SDK does **not** change your `applicationVersion` config field unless **you** choose to.
---
## 16. Support
When contacting Bespot support, include:
- SDK package version (e.g. `gatekeeper-web-sdk-1.0.0.tgz` → `1.0.0`)
- `applicationId` and `applicationVersion` from your config
- `error.name` and `error.message`
- Whether the problem occurs on `initialize` or `check`
- Browser name, version, and operating system
- Approximate time (UTC) of the failure
Authentication reference: [Bespot Authentication Guide](https://docs.bespot.com/api/auth).
---