# API overview
Source: https://docs.bespot.com/api/api
Overview of Bespot Gatekeeper APIs and how they fit into your architecture.
# API Reference
Gatekeeper API exposes a variety of resources (endpoints) for 3rd parties to consume. All connections are over HTTPS.
## HTTP Headers
Both the following HTTP headers are **required** for accessing all API resources:
* `Content-Type: application/json`
* `Authorization: Bearer {token}`
# Authentication
Source: https://docs.bespot.com/api/auth
Authentication model, credentials, and token handling for the API.
# Authenticate with Gatekeeper
Bespot authentication is based on OAuth 2.0 Client Credentials Flow, as defined in [RFC 6749, section 4.4](https://tools.ietf.org/html/rfc6749#section-4.4), for authenticating clients and authorise them to access protected resources. This page is organised to depict this flow, describe the related components and, finally, provide instructions for a client on how to achieve that.
## Components
### Client
The client is either Bespot's SDKs that request access to resources or any 3rd party server that needs to be authenticated and communicate securely with the system. The client needs to provide valid *credentials*, acquire an *access token* and, finally, use it for accessing protected resources.
### Gatekeeper Auth
Bespot Gatekeeper uses an external Auth server for authenticating clients. To achieve that, the OAuth 2.0 Client Credentials Flow is used. More specifically, a *client id* and a *client secret* need to be provided as input to the /oauth2/token endpoint. Then, if the credentials provided are valid, the authentication is successful, and an *access token* is returned. The *access token* is in the form of JWT based on [RFC 7519](https://datatracker.ietf.org/doc/html/rfc7519). Finally, this *access token* is used to retrieve resources from the Gatekeeper API (see below). The *access token* is only valid for a predefined period. If the *access token* expires, the client needs to start over with the authentication process and acquire a new one.
### Gatekeeper API
Bespot's Gatekeeper API is a collection of protected resources. To consume any Gatekeeper API resource, the `Authorization` HTTP header is **required**. Firstly, each client needs to be authenticated with the Gatekeeper Auth server, providing the authentication credentials and acquire a valid *access token*.
## Usage
See below the details of the authentication request to acquire the JWT *access token*.
### Resource \[POST]
`[{AUTH_SERVER_URL}/oauth2/token]`
The `AUTH_SERVER_URL` is provided securely along with the credentials.
### Request
**Headers**
* `Content-Type: application/x-www-form-urlencoded`
* `Authorization: Basic Base64Encode(client_id:client_secret)`
> **Example**
>
> The following is an authorization header for app client `djc98u3jiedmi283eu928` with client secret `abcdef01234567890`, using the Base64-encoded version of the string `djc98u3jiedmi283eu928:abcdef01234567890`:
Authorization: Basic ZGpjOTh1M2ppZWRtaTI4M2V1OTI4OmFiY2RlZjAxMjM0NTY3ODkw
**Body**
`grant_type` should also be set to `client_credentials` and passed as url encoded data.
### Response
**Body**
The response is in JSON format and has the following structure:
```json JWT theme={null}
{
"access_token": "{JWT-ACCESS-TOKEN}",
"expires_in": 86400,
"token_type": "Bearer"
}
```
```json theme={null}
```
### Example cURL
```shell theme={null}
curl --location '{AUTH_SERVER_URL}/oauth2/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'Authorization: Basic ZGpjOTh1M2ppZWRtaTI4M2V1OTI4OmFiY2RlZjAxMjM0NTY3ODkw' \
--data-urlencode 'grant_type=client_credentials'
```
# Authorization in Gatekeeper API
Bespot, along with access tokens, uses API keys to authorize different types of clients to access different types of resources.
* The acquired *access token* should be provided in each Gatekeeper API request for the client to be authorised to access the resources. The `Authorization` HTTP header should be filled with the acquired JWT *access token* prefixed with the following string: `Bearer `.
* The acquired *API Key* should be provided in each Gatekeeper API request to allow access to resources corresponding to specific client types.
## Security
OAuth 2.0 Client Credentials Flow is purposed for server-to-server communication. Client credentials must be securely stored server-side. However, when authentication is needed to be done client-side, e.g. from a device or browser, we encourage the use of a **server token broker** (see [Server Token Broker](https://docs.bespot.com/api/server-side-token-broker)) instead of using client credentials directly in your app.
## Usage
Each client connected to Gatekeeper API should provide both the acquired API Key and the acquired access token as HTTP headers in each request.
> **Example**
>
> `x-api-key: {X-API-KEY}`
>
> `Authorization: Bearer {JWT-ACCESS-TOKEN}`
The `X-API-KEY` is provided securely along with the credentials.
# Error codes
Source: https://docs.bespot.com/api/errors
API status codes, error responses, and troubleshooting guidance.
# Errors
Bespot API uses HTTP status codes to communicate errors. An `error_code`, `description` JSON structure is used. See below.
## 403 Forbidden
```json theme={null}
{
"error_code": "forbidden-user",
"description": "Invalid API Key"
}
```
# Server-to-server API
Source: https://docs.bespot.com/api/s2s
Server-to-server API integration for verification and policy decisions.
# Server-to-server integration
This page is a reference to all endpoints available for Server-to-server communication.
## Action Verification
Bespot Gatekeeper SDKs when used in customer's frontend applications (Web, Android & iOS) return an `(action, ticket)` tuple as a response. Typically, the policy *action* (evaluation result) would be sufficient for determining if a user is allowed to proceed further. However, there can be potential cases where this information can be tampered with or altered thus bypassing Bespot Gatekeeper's policy action. To ensure that the *action* information is valid, Bespot Gatekeeper API offers a server2server verification mechanism. The client's server collects the policy action `(action, ticket)` from its frontend applications, and then asks Bespot Gatekeeper API to verify it. *Action* information is cross-checked with the accompanied encrypted *ticket* information. *Ticket* information can only be decrypted by the Bespot Gatekeeper API server. If *ticket* or *action* information is tampered with or altered, verification fails. If verification succeeds, the Client's server can then communicate back to its frontend application that *action* information is verified and proceed.
### Resource \[POST]
`[/v2/s2s/verify/]`
### Request
```json theme={null}
{
"ticket": "string",
"action": "string",
"valid_for_at_most_millis": 0,
"application_package": "string"
}
```
where:
* `ticket` \[string] the encrypted ticket received from the SDK - *Required*
* `action` \[string] the policy action received from the SDK - *Required*
* `valid_for_at_most_millis` \[integer] the period in milliseconds that the action validity will be checked across. If there is no preference, `0` should be set. If `0` or a number `> 3000000` is set, then, it defaults to the system's maximum validity period of 300000 (5 minutes) for security reasons\* - *Required*
* `application_package` \[string] the frontend application's package or bundle identifier (received from Web, Android, iOS) - *Required*
\*For security reasons, we maintain a maximum threshold internally in order to defend against possible malware attacks (e.g. replay attacks).
### Response
```json verify theme={null}
{
"is_verified": true,
"action": "string",
"valid_for_at_most_millis": 0,
"application_package": "string",
"action_creation_date": "2024-09-24T06:55:50.089Z",
"verification_failed_reason": "string"
}
```
where:
* `is_verified` \[boolean] the verification result
* `action` \[string] the policy action received from the SDK. Values:
* `SAFE` *Everything is normal, no action needed*
* `MONITOR` *Keep monitoring user session to track any suspicious move*
* `LIMIT_ACCESS` *Should limit user access, and remove using some crucial features*
* `BLOCK` *Device should be blocked and immediately stop the application*
* `valid_for_at_most_millis` \[integer] the configured period in milliseconds that the action validity will be checked across
* `application_package` \[string] the frontend application's package or bundle identifier (received from Web, Android, iOS)
* `action_creation_date` \[ISO Date & Time with Timezone] the date and time (with timezone) the action was created in the first place
* `verification_failed_reason`: \[string] a descriptive string explaining the reason verification failed
### Example cURL
```shell theme={null}
curl --location --request POST 'https://antifraud.bespot.dev/v2/s2s/verify/' \
--header 'Authorization: Bearer TOKEN' \
--header 'Content-Type: application/json' \
--header 'x-api-key: API-KEY' \
--data '{
"ticket": "string",
"action": "string",
"valid_for_at_most_millis": 0,
"application_package": "string"
}'
```
```bash s2s theme={null}
curl --location --request POST 'https://antifraud.bespot.dev/v2/s2s/verify/' \
--header 'Authorization: Bearer TOKEN' \
--header 'Content-Type: application/json' \
--header 'x-api-key: API-KEY' \
--data '{
"ticket": "string",
"action": "string",
"valid_for_at_most_millis": 0,
"application_package": "string"
}'
```
# Server-side token broker
Source: https://docs.bespot.com/api/server-side-token-broker
Token retrieval flow, endpoint specification and code examples.
Gatekeeper SDKs for [iOS](https://docs.bespot.com/sdk/ios), [Android](https://docs.bespot.com/sdk/android), and [Web](https://docs.bespot.com/sdk/web) support initialization using a JWT access token instead of the OAuth 2.0 client credentials (`auth url`, `client id`, `client secret`) directly. This is the recommended approach for client-side applications, because storing client credentials on a device or in a browser is not secure.
Your backend must therefore expose an endpoint that returns a valid JWT access token, acting as a **server-side token broker**. Your client applications must call this endpoint before SDK initialization; your backend acquires the token from Gatekeeper using the [OAuth 2.0 Client Credentials Flow](https://docs.bespot.com/api/auth) while **securely** keeps the client credentials server-side; the valid JWT access token is returned to your client applications; the Gatekeeper SDK is initialized with this token.
Your client applications are responsible for timely updating the JWT access token before expiring.
## Token Retrieval Flow
The sequence diagram illustrates how your Client App obtains a JWT access token required to initialize the Gatekeeper SDK, while ensuring that your Client Backend does not request a new token from the Gatekeeper Auth Server unnecessarily.
1. **Token request from the Client App** Your Client App (iOS, Android, or Web) requests a Gatekeeper JWT access token from your Client Backend through the `/api/gatekeeper-token` endpoint. The `application_package` parameter identifies the application for which the token is required.
2. **Check the token cache** Your Client Backend checks the Token Cache for an existing access token associated with the specified `application_package`.
3. **Use a valid cached token** If a valid, non-expired token is found in the cache, your Client Backend retrieves it and uses it to fulfill your Client App’s request. No request to the Gatekeeper Auth Server is required.
4. **Obtain a new token when necessary** If no valid token is available — for example, because the token is missing from the cache or has expired — your Client Backend requests a new JWT access token from the Gatekeeper Auth Server using the OAuth 2.0 Client Credentials flow via the `/oauth2/token` endpoint (see [OAuth 2.0 Client Credentials Flow](https://docs.bespot.com/api/auth)).
5. **Cache the new token** The Gatekeeper Auth Server returns the JWT access token together with its `expires_in` value. Your Client Backend should store the token in your Token Cache and keeps it available until its expiry.
6. **Return the token to the Client App** Your Client Backend returns the JWT access token to your Client App, regardless of whether the token was retrieved from the cache or newly obtained from the Gatekeeper Auth Server.
Finally, your Client App initializes the Gatekeeper SDK using the JWT access token. The Gatekeeper SDK can then use this token when communicating with Gatekeeper services in order to be authorized to access resources.
This caching mechanism reduces unnecessary authentication requests to the Gatekeeper Auth Server, speeds up the response times and allows multiple requests from the Client App to reuse the same valid access token - for the specific *`application_package`* - until it expires.
## Endpoint Specification
### Resource \[GET]
`[{CLIENT_SERVER_URL}/api/gatekeeper-token]`
Replace `{CLIENT_SERVER_URL}` with your application’s backend url.
### Query Parameters
| Parameter | Type | Required | Description |
| :-------------------- | :----- | :------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `application_package` | string | Yes | Identifies the client application requesting the token. Use the **Android application package name** (e.g. `com.example.myapp`), the **iOS bundle identifier** (e.g. `com.example.MyApp`), or the **Web application domain** (e.g. `app.example.com`). |
Your backend uses this value to select the correct OAuth 2.0 client credentials for the corresponding platform.
### Response
**Status:** `200 OK`
**Body**
Return the JWT access token formatted in JSON.
JSON:
```json theme={null}
{
"access_token": "{JWT-ACCESS-TOKEN}"
}
```
### Error Responses
Return appropriate HTTP status codes when:
* `application_package` is missing or unknown (`400 Bad Request`)
* Gatekeeper credentials for the given application are not configured (`401 Unauthorized`)
* The Gatekeeper auth server rejects the request (`502 Bad Gateway` or `503 Service Unavailable`)
## Backend Responsibilities
1. **Store credentials securely.** Keep the `AUTH_SERVER_URL`, `client_id`, and `client_secret` for each application platform in a secure store (environment variables, secrets manager, or encrypted configuration). Never expose them to client applications.
2. **Map `application_package` to credentials.** Maintain a lookup from the incoming `application_package`value to the OAuth 2.0 credentials registered with Gatekeeper for that platform.
3. **Acquire a JWT from Gatekeeper.** When no valid cached token is available, request a new access token from `{AUTH_SERVER_URL}/oauth2/token` as described in [Authenticate with Gatekeeper](https://docs.bespot.com/api/auth).
4. **Cache valid tokens.** Client applications may call this endpoint frequently. Cache each JWT until it expires. Use the `expires_in` value from the Gatekeeper auth response, or decode the JWT and read the `exp` claim. Invalidate the cache entry when the token expires and request a fresh token on the next call.
## Caching Strategy
* **Cache key:** Use `application_package` as the cache key so each platform/application has its own token.
* **TTL:** Set the cache entry lifetime to the token’s remaining validity. Subtract a small safety margin (e.g. 30–60 seconds) from `expires_in` to avoid returning a token that expires before the SDK uses it.
* **Invalidation:** When a cached token expires, discard it and fetch a new one from Gatekeeper on the next request.
* **Storage:** An in-memory cache is sufficient for a single-instance backend. Use a shared cache (e.g. Redis) if your backend runs on multiple instances.
## Code Examples
The examples below implement a `GET /api/gatekeeper-token` endpoint with token caching. Replace placeholder values with the Gatekeeper auth server URL and credentials provided in [Gatekeeper](https://gatekeeper.bespot.com/) platform.
```python theme={null}
import base64
import time
from dataclasses import dataclass
from typing import Dict, Optional
import requests
from flask import Flask, request, jsonify
app = Flask(__name__)
AUTH_SERVER_URL = "https://auth.example.com"
CREDENTIALS_BY_PACKAGE: Dict[str, Dict[str, str]] = {
"com.example.myapp": {
"client_id": "android-client-id",
"client_secret": "android-client-secret",
},
"com.example.MyApp": {
"client_id": "ios-client-id",
"client_secret": "ios-client-secret",
},
"app.example.com": {
"client_id": "web-client-id",
"client_secret": "web-client-secret",
},
}
CACHE_SAFETY_MARGIN_SECONDS = 60
@dataclass
class CachedToken:
access_token: str
expires_at: float
token_cache: Dict[str, CachedToken] = {}
def fetch_gatekeeper_token(client_id: str, client_secret: str) -> CachedToken:
credentials = f"{client_id}:{client_secret}".encode("utf-8")
authorization = base64.b64encode(credentials).decode("utf-8")
response = requests.post(
f"{AUTH_SERVER_URL}/oauth2/token",
headers={
"Content-Type": "application/x-www-form-urlencoded",
"Authorization": f"Basic {authorization}",
},
data={"grant_type": "client_credentials"},
timeout=10,
)
response.raise_for_status()
payload = response.json()
expires_in = int(payload["expires_in"])
expires_at = time.time() + max(expires_in - CACHE_SAFETY_MARGIN_SECONDS, 0)
return CachedToken(
access_token=payload["access_token"],
expires_at=expires_at,
)
def get_cached_token(application_package: str) -> str:
cached = token_cache.get(application_package)
if cached and cached.expires_at > time.time():
return cached.access_token
credentials = CREDENTIALS_BY_PACKAGE.get(application_package)
if not credentials:
raise KeyError(f"Unknown application_package: {application_package}")
cached = fetch_gatekeeper_token(
credentials["client_id"],
credentials["client_secret"],
)
token_cache[application_package] = cached
return cached.access_token
@app.get("/api/gatekeeper-token")
def gatekeeper_token():
application_package = request.args.get("application_package")
if not application_package:
return jsonify({"error": "application_package is required"}), 400
try:
access_token = get_cached_token(application_package)
except KeyError as exc:
return jsonify({"error": str(exc)}), 400
except requests.RequestException:
return jsonify({"error": "Failed to acquire token from Gatekeeper"}), 502
return jsonify({"access_token": access_token})
```
```kotlin theme={null}
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.client.request.forms.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.serialization.Serializable
import java.time.Instant
import java.util.Base64
import java.util.concurrent.ConcurrentHashMap
private const val AUTH_SERVER_URL = "https://auth.example.com"
private const val CACHE_SAFETY_MARGIN_SECONDS = 60L
private val credentialsByPackage = mapOf(
"com.example.myapp" to ClientCredentials("android-client-id", "android-client-secret"),
"com.example.MyApp" to ClientCredentials("ios-client-id", "ios-client-secret"),
"app.example.com" to ClientCredentials("web-client-id", "web-client-secret"),
)
data class ClientCredentials(val clientId: String, val clientSecret: String)
data class CachedToken(val accessToken: String, val expiresAt: Instant)
@Serializable
data class TokenResponse(val access_token: String, val expires_in: Long)
@Serializable
data class GatekeeperTokenResponse(val access_token: String)
private val tokenCache = ConcurrentHashMap()
private val httpClient = HttpClient(CIO) {
install(ContentNegotiation) { json() }
}
suspend fun fetchGatekeeperToken(credentials: ClientCredentials): CachedToken {
val basicAuth = Base64.getEncoder().encodeToString(
"${credentials.clientId}:${credentials.clientSecret}".toByteArray()
)
val response: TokenResponse = httpClient.post("$AUTH_SERVER_URL/oauth2/token") {
header(HttpHeaders.Authorization, "Basic $basicAuth")
contentType(ContentType.Application.FormUrlEncoded)
setBody(FormDataContent(Parameters.build {
append("grant_type", "client_credentials")
}))
}.body()
val expiresAt = Instant.now().plusSeconds(
maxOf(response.expires_in - CACHE_SAFETY_MARGIN_SECONDS, 0)
)
return CachedToken(response.access_token, expiresAt)
}
suspend fun getCachedToken(applicationPackage: String): String {
val cached = tokenCache[applicationPackage]
if (cached != null && cached.expiresAt.isAfter(Instant.now())) {
return cached.accessToken
}
val credentials = credentialsByPackage[applicationPackage]
?: error("Unknown application_package: $applicationPackage")
val freshToken = fetchGatekeeperToken(credentials)
tokenCache[applicationPackage] = freshToken
return freshToken.accessToken
}
fun main() {
embeddedServer(Netty, port = 8080) {
routing {
get("/api/gatekeeper-token") {
val applicationPackage = call.request.queryParameters["application_package"]
if (applicationPackage.isNullOrBlank()) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "application_package is required"))
return@get
}
try {
val accessToken = getCachedToken(applicationPackage)
call.respond(GatekeeperTokenResponse(accessToken))
} catch (error: IllegalStateException) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to error.message))
} catch (error: Exception) {
call.respond(HttpStatusCode.BadGateway, mapOf("error" to "Failed to acquire token from Gatekeeper"))
}
}
}
}.start(wait = true)
}
```
```java theme={null}
import org.springframework.http.*;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Base64;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@RestController
public class GatekeeperTokenController {
private static final String AUTH_SERVER_URL = "https://auth.example.com";
private static final long CACHE_SAFETY_MARGIN_SECONDS = 60;
private final RestTemplate restTemplate = new RestTemplate();
private final Map credentialsByPackage = Map.of(
"com.example.myapp", new ClientCredentials("android-client-id", "android-client-secret"),
"com.example.MyApp", new ClientCredentials("ios-client-id", "ios-client-secret"),
"app.example.com", new ClientCredentials("web-client-id", "web-client-secret")
);
private final Map tokenCache = new ConcurrentHashMap<>();
@GetMapping("/api/gatekeeper-token")
public ResponseEntity> gatekeeperToken(@RequestParam(value = "application_package", required = false) String applicationPackage) {
if (applicationPackage == null || applicationPackage.isBlank()) {
return ResponseEntity.badRequest().body(Map.of("error", "application_package is required"));
}
ClientCredentials credentials = credentialsByPackage.get(applicationPackage);
if (credentials == null) {
return ResponseEntity.badRequest().body(Map.of("error", "Unknown application_package: " + applicationPackage));
}
try {
String accessToken = getCachedToken(applicationPackage, credentials);
return ResponseEntity.ok(Map.of("access_token", accessToken));
} catch (RestClientException ex) {
return ResponseEntity.status(HttpStatus.BAD_GATEWAY)
.body(Map.of("error", "Failed to acquire token from Gatekeeper"));
}
}
private String getCachedToken(String applicationPackage, ClientCredentials credentials) {
CachedToken cached = tokenCache.get(applicationPackage);
if (cached != null && cached.expiresAt().isAfter(Instant.now())) {
return cached.accessToken();
}
CachedToken freshToken = fetchGatekeeperToken(credentials);
tokenCache.put(applicationPackage, freshToken);
return freshToken.accessToken();
}
private CachedToken fetchGatekeeperToken(ClientCredentials credentials) {
String basicAuth = Base64.getEncoder().encodeToString(
(credentials.clientId() + ":" + credentials.clientSecret()).getBytes(StandardCharsets.UTF_8)
);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.set(HttpHeaders.AUTHORIZATION, "Basic " + basicAuth);
MultiValueMap body = new LinkedMultiValueMap<>();
body.add("grant_type", "client_credentials");
ResponseEntity response = restTemplate.exchange(
AUTH_SERVER_URL + "/oauth2/token",
HttpMethod.POST,
new HttpEntity<>(body, headers),
TokenResponse.class
);
TokenResponse tokenResponse = response.getBody();
if (tokenResponse == null || tokenResponse.accessToken() == null) {
throw new RestClientException("Empty token response from Gatekeeper");
}
Instant expiresAt = Instant.now().plusSeconds(
Math.max(tokenResponse.expiresIn() - CACHE_SAFETY_MARGIN_SECONDS, 0)
);
return new CachedToken(tokenResponse.accessToken(), expiresAt);
}
private record ClientCredentials(String clientId, String clientSecret) {}
private record CachedToken(String accessToken, Instant expiresAt) {}
private record TokenResponse(String accessToken, long expiresIn) {}
}
```
```ruby theme={null}
require "sinatra"
require "net/http"
require "json"
require "base64"
require "uri"
AUTH_SERVER_URL = "https://auth.example.com"
CACHE_SAFETY_MARGIN_SECONDS = 60
CREDENTIALS_BY_PACKAGE = {
"com.example.myapp" => { client_id: "android-client-id", client_secret: "android-client-secret" },
"com.example.MyApp" => { client_id: "ios-client-id", client_secret: "ios-client-secret" },
"app.example.com" => { client_id: "web-client-id", client_secret: "web-client-secret" }
}.freeze
$token_cache = {}
def fetch_gatekeeper_token(client_id, client_secret)
uri = URI("#{AUTH_SERVER_URL}/oauth2/token")
request = Net::HTTP::Post.new(uri)
request["Content-Type"] = "application/x-www-form-urlencoded"
request["Authorization"] = "Basic #{Base64.strict_encode64("#{client_id}:#{client_secret}")}"
request.body = URI.encode_www_form(grant_type: "client_credentials")
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |http|
http.request(request)
end
raise "Gatekeeper auth failed: #{response.code}" unless response.is_a?(Net::HTTPSuccess)
payload = JSON.parse(response.body)
expires_at = Time.now.to_i + [payload["expires_in"].to_i - CACHE_SAFETY_MARGIN_SECONDS, 0].max
{
access_token: payload["access_token"],
expires_at: expires_at
}
end
def get_cached_token(application_package)
cached = $token_cache[application_package]
return cached[:access_token] if cached && cached[:expires_at] > Time.now.to_i
credentials = CREDENTIALS_BY_PACKAGE[application_package]
raise ArgumentError, "Unknown application_package: #{application_package}" unless credentials
fresh_token = fetch_gatekeeper_token(credentials[:client_id], credentials[:client_secret])
$token_cache[application_package] = fresh_token
fresh_token[:access_token]
end
get "/api/gatekeeper-token" do
content_type :json
application_package = params["application_package"]
halt 400, { error: "application_package is required" }.to_json if application_package.nil? || application_package.empty?
begin
access_token = get_cached_token(application_package)
{ access_token: access_token }.to_json
rescue ArgumentError => e
halt 400, { error: e.message }.to_json
rescue StandardError
halt 502, { error: "Failed to acquire token from Gatekeeper" }.to_json
end
end
```
```go theme={null}
package main
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"sync"
"time"
)
const (
authServerURL = "https://auth.example.com"
cacheSafetyMargin = 60 * time.Second
)
type clientCredentials struct {
ClientID string
ClientSecret string
}
type cachedToken struct {
AccessToken string
ExpiresAt time.Time
}
type tokenResponse struct {
AccessToken string `json:"access_token"`
ExpiresIn int64 `json:"expires_in"`
}
var (
credentialsByPackage = map[string]clientCredentials{
"com.example.myapp": {ClientID: "android-client-id", ClientSecret: "android-client-secret"},
"com.example.MyApp": {ClientID: "ios-client-id", ClientSecret: "ios-client-secret"},
"app.example.com": {ClientID: "web-client-id", ClientSecret: "web-client-secret"},
}
tokenCache = struct {
sync.RWMutex
entries map[string]cachedToken
}{
entries: make(map[string]cachedToken),
}
)
func fetchGatekeeperToken(credentials clientCredentials) (cachedToken, error) {
form := url.Values{}
form.Set("grant_type", "client_credentials")
req, err := http.NewRequest(http.MethodPost, authServerURL+"/oauth2/token", strings.NewReader(form.Encode()))
if err != nil {
return cachedToken{}, err
}
basicAuth := base64.StdEncoding.EncodeToString(
[]byte(credentials.ClientID + ":" + credentials.ClientSecret),
)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Authorization", "Basic "+basicAuth)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return cachedToken{}, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return cachedToken{}, fmt.Errorf("gatekeeper auth failed: %s", string(body))
}
var payload tokenResponse
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
return cachedToken{}, err
}
expiresAt := time.Now().Add(time.Duration(payload.ExpiresIn)*time.Second - cacheSafetyMargin)
if expiresAt.Before(time.Now()) {
expiresAt = time.Now()
}
return cachedToken{
AccessToken: payload.AccessToken,
ExpiresAt: expiresAt,
}, nil
}
func getCachedToken(applicationPackage string) (string, error) {
tokenCache.RLock()
cached, ok := tokenCache.entries[applicationPackage]
tokenCache.RUnlock()
if ok && time.Now().Before(cached.ExpiresAt) {
return cached.AccessToken, nil
}
credentials, ok := credentialsByPackage[applicationPackage]
if !ok {
return "", fmt.Errorf("unknown application_package: %s", applicationPackage)
}
freshToken, err := fetchGatekeeperToken(credentials)
if err != nil {
return "", err
}
tokenCache.Lock()
tokenCache.entries[applicationPackage] = freshToken
tokenCache.Unlock()
return freshToken.AccessToken, nil
}
func gatekeeperTokenHandler(w http.ResponseWriter, r *http.Request) {
applicationPackage := r.URL.Query().Get("application_package")
if applicationPackage == "" {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "application_package is required"})
return
}
accessToken, err := getCachedToken(applicationPackage)
if err != nil {
if strings.Contains(err.Error(), "unknown application_package") {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusBadGateway, map[string]string{"error": "Failed to acquire token from Gatekeeper"})
return
}
writeJSON(w, http.StatusOK, map[string]string{"access_token": accessToken})
}
func writeJSON(w http.ResponseWriter, status int, payload any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(payload)
}
func main() {
http.HandleFunc("/api/gatekeeper-token", gatekeeperTokenHandler)
if err := http.ListenAndServe(":8080", nil); err != nil && !errors.Is(err, http.ErrServerClosed) {
panic(err)
}
}
```
```rust theme={null}
use axum::{
extract::Query,
http::StatusCode,
response::{IntoResponse, Response},
routing::get,
Json, Router,
};
use base64::{engine::general_purpose::STANDARD, Engine};
use reqwest::header::{AUTHORIZATION, CONTENT_TYPE};
use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
sync::{Arc, RwLock},
time::{Duration, Instant},
};
use tokio::net::TcpListener;
const AUTH_SERVER_URL: &str = "https://auth.example.com";
const CACHE_SAFETY_MARGIN: Duration = Duration::from_secs(60);
#[derive(Clone)]
struct ClientCredentials {
client_id: String,
client_secret: String,
}
#[derive(Clone)]
struct CachedToken {
access_token: String,
expires_at: Instant,
}
#[derive(Clone)]
struct AppState {
credentials_by_package: HashMap,
token_cache: Arc>>,
http_client: reqwest::Client,
}
#[derive(Deserialize)]
struct TokenQuery {
application_package: Option,
}
#[derive(Deserialize)]
struct TokenResponse {
access_token: String,
expires_in: u64,
}
#[derive(Serialize)]
struct GatekeeperTokenResponse {
access_token: String,
}
#[derive(Serialize)]
struct ErrorResponse {
error: String,
}
async fn fetch_gatekeeper_token(
http_client: &reqwest::Client,
credentials: &ClientCredentials,
) -> Result {
let basic_auth = STANDARD.encode(format!(
"{}:{}",
credentials.client_id, credentials.client_secret
));
let response = http_client
.post(format!("{AUTH_SERVER_URL}/oauth2/token"))
.header(CONTENT_TYPE, "application/x-www-form-urlencoded")
.header(AUTHORIZATION, format!("Basic {basic_auth}"))
.body("grant_type=client_credentials")
.send()
.await?
.error_for_status()?
.json::()
.await?;
let ttl = Duration::from_secs(response.expires_in).saturating_sub(CACHE_SAFETY_MARGIN);
Ok(CachedToken {
access_token: response.access_token,
expires_at: Instant::now() + ttl,
})
}
async fn get_cached_token(
state: &AppState,
application_package: &str,
) -> Result {
{
let cache = state.token_cache.read().unwrap();
if let Some(cached) = cache.get(application_package) {
if cached.expires_at > Instant::now() {
return Ok(cached.access_token.clone());
}
}
}
let credentials = state
.credentials_by_package
.get(application_package)
.cloned()
.ok_or_else(|| format!("Unknown application_package: {application_package}"))?;
let fresh_token = fetch_gatekeeper_token(&state.http_client, &credentials)
.await
.map_err(|_| "Failed to acquire token from Gatekeeper".to_string())?;
let access_token = fresh_token.access_token.clone();
state
.token_cache
.write()
.unwrap()
.insert(application_package.to_string(), fresh_token);
Ok(access_token)
}
async fn gatekeeper_token_handler(
axum::extract::State(state): axum::extract::State,
Query(query): Query,
) -> Response {
let Some(application_package) = query.application_package.filter(|value| !value.is_empty()) else {
return (
StatusCode::BAD_REQUEST,
Json(ErrorResponse {
error: "application_package is required".to_string(),
}),
)
.into_response();
};
match get_cached_token(&state, &application_package).await {
Ok(access_token) => (
StatusCode::OK,
Json(GatekeeperTokenResponse { access_token }),
)
.into_response(),
Err(message) if message.starts_with("Unknown application_package") => (
StatusCode::BAD_REQUEST,
Json(ErrorResponse { error: message }),
)
.into_response(),
Err(_) => (
StatusCode::BAD_GATEWAY,
Json(ErrorResponse {
error: "Failed to acquire token from Gatekeeper".to_string(),
}),
)
.into_response(),
}
}
#[tokio::main]
async fn main() {
let mut credentials_by_package = HashMap::new();
credentials_by_package.insert(
"com.example.myapp".to_string(),
ClientCredentials {
client_id: "android-client-id".to_string(),
client_secret: "android-client-secret".to_string(),
},
);
credentials_by_package.insert(
"com.example.MyApp".to_string(),
ClientCredentials {
client_id: "ios-client-id".to_string(),
client_secret: "ios-client-secret".to_string(),
},
);
credentials_by_package.insert(
"app.example.com".to_string(),
ClientCredentials {
client_id: "web-client-id".to_string(),
client_secret: "web-client-secret".to_string(),
},
);
let state = AppState {
credentials_by_package,
token_cache: Arc::new(RwLock::new(HashMap::new())),
http_client: reqwest::Client::new(),
};
let app = Router::new()
.route("/api/gatekeeper-token", get(gatekeeper_token_handler))
.with_state(state);
let listener = TcpListener::bind("0.0.0.0:8080").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
```
Code examples above are **AI generated**. Consult your development team to integrate the new endpoint properly in your backend code.
# Blueprint API
Source: https://docs.bespot.com/blueprint/api/api
Bespot Blueprint API offers a range of capabilities supporting and extending the Bespot SDK. Authentication credentials need to be available in order to consume the resources of this API. Authentication credentials are different for each environment.
## API Reference
Blueprint API exposes a variety of resources (endpoints) for 3rd parties to consume. All connections are over HTTPS.
## HTTP Headers
* `Content-Type: application/json` An HTTP header is **required** for all resources.
* `Authorization: Bearer {token}` The bearer token is required for authentication and access to resources.
# Area Metrics
Source: https://docs.bespot.com/blueprint/api/area-metrics
This section describes endpoints available for Store Area Metrics.
The following table briefly presents the categories, while the following paragraphs describe in detail all available resources.
| Area Metric | Description |
| ------------------- | -------------------------------------------------------------------- |
| Unique Users | Unique Device IDs per store area |
| Average Screen Time | The average time spent per user per zone. |
| Area Traffic | Benchmark of traffic distribution (%) across the store's areas/zones |
### Availability (granularity)
All area metrics are available in **daily** granularity. It is proposed to fetch area metrics in daily increments. For example, an import flow can be run once daily and fetch all metrics available for the previous day (using the appropriate time filters).
### Pagination
All area metrics resources support pagination. Due to increased data volume, adding pagination to these resources allows fetching data in batches (pages) for a specific time interval. In each paginated response you will find the following properties:
* the `count` property shows the total results of the query
* the `next` property contains the link of the subsequent page (in case there is any) otherwise it has `null` value
* the `previous` property operated similarly to the `next` property
* the `results` property contains the array of sessions either for a page or for the entire result
**Key takeways:** If `page_size` (default or set by the client) > `count` then only one page of results is returned and `next` and `previous` properties are `null`. To consume all pages, the client needs to continue fetching page-by-page using the link provided within the `next` property until `null` value is found (last page - all sessions fetched).
***
## Unique Users
This resource returns the unique users (Unique Device IDs) per zone for a specific store and time interval. The response contains a list of results grouped by store and date.
### Resource \[GET]
`[/bi/area-metrics/unique-users/]`
### URL Parameters
* `store_code` The store code - *Optional*
* `datetime_gte` A date string (in **UTC**) to be used as greater than or equal for filtering - *Required*
* `datetime_lte` A date string (in **UTC**) to be used as less than or equal for filtering - *Required*
* `page_size` The number of results in each page of the response - *Optional.* Default value is used when the parameter is not provided
* `page` The page number of the response - *Optional.* Only required when the response is paginated
*`Note: Default page_size = 1000. Maximum page_size = 10000`*
### Response
In each result object, the following data are available:
* `store_code` The store code - *String*
* `date` The day for which the metric was generated - *ISO 8601 Date and time with timezone*
* `granularity` The time granularity for the metric - currently available: `daily` - *String*
* `values` An array containing the unique user values per zone - *Array*
* `zone_name` The name of the zone - *String*
* `unique_users` The number of unique users for this zone - *Int*
```json theme={null}
{
"next": "http://blueprint.bespot.io/api/bi/area-metrics/unique-users/?datetime_gte=2024-01-22T00%3A00%3A00Z&datetime_lte=2024-01-24T00%3A00%3A00Z&page=2&page_size=3",
"previous": null,
"count": 497,
"results": [
{
"store_code": "AHQ21000",
"date": "2024-01-24T00:00:00+02:00",
"granularity": "daily",
"values": [
{
"zone": "STOREFRONT (SIDEWALK)",
"unique_users": 14
},
{
"zone": "[10] POS",
"unique_users": 5
},
{
"zone": "[2] LOTTERY ZONE",
"unique_users": 21
}
]
},
{
"store_code": "AHQ21000",
"date": "2024-01-23T00:00:00+02:00",
"granularity": "daily",
"values": [
{
"zone": "STOREFRONT (SIDEWALK)",
"unique_users": 12
},
{
"zone": "[10] POS",
"unique_users": 5
},
{
"zone": "[2] LOTTERY ZONE",
"unique_users": 22
}
]
},
{
"store_code": "AHQ21000",
"date": "2024-01-24T00:00:00+02:00",
"granularity": "daily",
"values": [
{
"zone": "POS",
"unique_users": 19
},
{
"zone": "[7] COFFEE BAR",
"unique_users": 24
},
{
"zone": "[8] FAST SERVING ZONE",
"unique_users": 6
}
]
}
]
}
```
### Example cURL
```
curl --location 'https://blueprint.bespot.io/api/bi/area-metrics/unique-users?datetime_gte=2024-01-22T00%3A00%3A00Z&datetime_lte=2024-01-24T00%3A00%3A00Z&page_size=3' \
--header 'Authorization: Basic BASE64_ENCODED_CREDENTIALS'
```
***
## Average Screen Time
This resource returns the average time a user interacts with the app in a specific zone of a specific store and time interval. The response contains a list of results grouped by store and date.
### Resource \[GET]
`[/bi/area-metrics/avg-screen-time/]`
### URL Parameters
* `store_code` The store code - *Optional*
* `datetime_gte` A date string (in **UTC**) to be used as greater than or equal for filtering - *Required*
* `datetime_lte` A date string (in **UTC**) to be used as less than or equal for filtering - *Required*
* `page_size` The number of results in each page of the response - *Optional.* Default value is used when the parameter is not provided
* `page` The page number of the response - *Optional.* Only required when the response is paginated
*`Note: Default page_size = 1000. Maximum page_size = 10000`*
### Response
In each result object, the following data are available:
* `store_code` The store code - *String*
* `date` The day for which the metric was generated - *ISO 8601 Date and time with timezone*
* `granularity` The time granularity for the metric - currently available: `daily` - *String*
* `values` An array containing the average screen time per zone - *Array*
* `zone_name` The name of the zone - *String*
* `avg_screen_time` average screen time users spend in this zone - *String*
```json theme={null}
{
"next": "http://blueprint.bespot.io/api/bi/area-metrics/avg-screen-time/?datetime_gte=2024-01-22T00%3A00%3A00Z&datetime_lte=2024-01-24T00%3A00%3A00Z&page=2&page_size=3",
"previous": null,
"count": 497,
"results": [
{
"store_code": "AHQ21000",
"date": "2024-01-24T00:00:00+02:00",
"granularity": "daily",
"values": [
{
"zone": "STOREFRONT (SIDEWALK)",
"avg_screen_time": "00:00:17"
},
{
"zone": "[10] POS",
"avg_screen_time": "00:00:10"
},
{
"zone": "[2] LOTTERY ZONE",
"avg_screen_time": "00:00:42"
},
{
"zone": "[4] CUSTOMER CARE",
"avg_screen_time": "00:00:46"
}
]
},
{
"store_code": "AHQ21000",
"date": "2024-01-23T00:00:00+02:00",
"granularity": "daily",
"values": [
{
"zone": "STOREFRONT (SIDEWALK)",
"avg_screen_time": "00:00:07"
},
{
"zone": "[10] POS",
"avg_screen_time": "00:00:06"
},
{
"zone": "[2] LOTTERY ZONE",
"avg_screen_time": "00:00:42"
},
{
"zone": "[4] CUSTOMER CARE",
"avg_screen_time": "00:00:46"
}
]
},
{
"store_code": "AHQ21000",
"date": "2024-01-24T00:00:00+02:00",
"granularity": "daily",
"values": [
{
"zone": "STOREFRONT (SIDEWALK)",
"avg_screen_time": "00:00:14"
},
{
"zone": "[2] LOTTERY ZONE",
"avg_screen_time": "00:00:14"
},
{
"zone": "[7] COFFEE BAR",
"avg_screen_time": "00:00:50"
},
{
"zone": "[8] FAST SERVING ZONE",
"avg_screen_time": "00:00:46"
}
]
}
]
}
```
*`Note: The avg_screen_time metric value is represented in the following string format: "HH:MM:SS". Hours, minutes and seconds.`*
### Example cURL
```
curl --location 'https://blueprint.bespot.io/api/bi/area-metrics/avg-screen-time?datetime_gte=2024-01-22T00%3A00%3A00Z&datetime_lte=2024-01-24T00%3A00%3A00Z&page_size=3' \
--header 'Authorization: Basic BASE64_ENCODED_CREDENTIALS'
```
***
## Area Traffic
This resource returns the percentage of a user's total time spent in each zone for a specific store and time interval. The sum of all area percentages should be 1 (meaning 100%). The response contains a list of results grouped by store and date. The *area traffic* metric serves as a benchmark for a store's zones.
### Resource \[GET]
`[/bi/area-metrics/traffic/]`
### URL Parameters
* `store_code` The store code - *Optional*
* `datetime_gte` A date string (in **UTC**) to be used as greater than or equal for filtering - *Required*
* `datetime_lte` A date string (in **UTC**) to be used as less than or equal for filtering - *Required*
* `page_size` The number of results in each page of the response - *Optional.* Default value is used when the parameter is not provided
* `page` The page number of the response - *Optional.* Only required when the response is paginated
*`Note: Default page_size = 1000. Maximum page_size = 10000`*
### Response
In each result object, the following data are available:
* `store_code` The store code - *String*
* `date` The day for which the metric was generated - *ISO 8601 Date and time with timezone*
* `granularity` The time granularity for the metric - currently available: `daily` - *String*
* `values` An array containing traffic percentage values per zone - *Array*
* `zone_name` The name of the zone/area - *String*
* `traffic_pct` The percentage of the time spent by users in this zone (in comparison with the rest of the zones) - *String*
```json theme={null}
{
"next": "http://blueprint.bespot.io/api/bi/area-metrics/traffic/?datetime_gte=2024-01-22T00%3A00%3A00Z&datetime_lte=2024-01-24T00%3A00%3A00Z&page=2&page_size=3",
"previous": null,
"count": 497,
"results": [
{
"store_code": "AHQ21000",
"date": "2024-01-24T00:00:00+02:00",
"granularity": "daily",
"values": [
{
"zone": "STOREFRONT (SIDEWALK)",
"traffic_pct": "0.09395"
},
{
"zone": "[10] POS",
"traffic_pct": "0.00977"
},
{
"zone": "[2] LOTTERY ZONE",
"traffic_pct": "0.89628"
}
]
},
{
"store_code": "AHQ21000",
"date": "2024-01-23T00:00:00+02:00",
"granularity": "daily",
"values": [
{
"zone": "STOREFRONT (SIDEWALK)",
"traffic_pct": "0.04140"
},
{
"zone": "[10] POS",
"traffic_pct": "0.01084"
},
{
"zone": "[2] LOTTERY ZONE",
"traffic_pct": "0.94776"
}
]
},
{
"store_code": "AHQ21000",
"date": "2024-01-24T00:00:00+02:00",
"granularity": "daily",
"values": [
{
"zone": "STOREFRONT (SIDEWALK)",
"traffic_pct": "0.08840"
},
{
"zone": "[7] COFFEE BAR",
"traffic_pct": "0.90860"
},
{
"zone": "[8] FAST SERVING ZONE",
"traffic_pct": "0.00300"
}
]
}
]
}
```
*`Note: The traffic_pct metric value is represented by a string in the following format: "X.XXXXX" indicating a numeric value between 0 and 1 with a precision of 5 decimal points.`*
### Example cURL
```
curl --location 'https://blueprint.bespot.io/api/bi/area-metrics/traffic?datetime_gte=2024-01-22T00%3A00%3A00Z&datetime_lte=2024-01-24T00%3A00%3A00Z&page_size=3' \
--header 'Authorization: Basic BASE64_ENCODED_CREDENTIALS'
```
# Authentication
Source: https://docs.bespot.com/blueprint/api/auth
How to authenticate with the Blueprint API
Bespot API supports both Basic Authentication and JWT Authentication over HTTPS. While both authentication methods are supported, API consumers are **strongly** advised to use JWT method to authenticate. To consume any resource the `Authorization` HTTP header is **required**.
## Basic Authentication
To consume an endpoint using Basic Authentication, the `Authorization` HTTP header should be filled with prefixed with the following string: `Basic ` and the available credentials encoded in *Base64* format.
`Authorization: Basic BASE64_ENCODED_CREDENTIALS`
## JWT Authentication
Before consuming any endpoint, each client needs to be authenticated with the API providing the authentication credentials. If the authentication is successful, a JWT `access` token is provided. This token can, then, be used in each subsequent API request so for the client to be authorised to access each the resource.
### GET JWT ACCESS TOKEN
See below the details of the authentication request to acquire the JWT *access* token.
### Resource \[POST]
`[/auth/jwt/token/]`
### Request
```
{
"username": "test-username",
"password": "test-password",
}
```
### Response
```
{
"access": "eyJhbGciOiJIUzI1NiJ9.eyJSb2xlIjoiZ2VydGdlIiwiSXNzdWVyIjoiZHNmIiwiVXNlcm5hbWUiOiJkZnNmZHMiLCJleHAiOjE3MDgwNzgwNTIsImlhdCI6MTcwODA3ODA1Mn0.QzC_fwqMsWUJS9VBiqLt_PJnQaCLnOI3B-xOEdK6Ops",
"refresh": "eyJhbGciOiJIUzI1NiJ9.eyJSb2xlIjoiQWRtaW4iLCJJc3N1ZXIiOiJJc3N1ZXIiLCJVc2VybmFtZSI6IkphdmFJblVzZSIsImV4cCI6MTcwODA3ODA1MiwiaWF0IjoxNzA4MDc4MDUyfQ.qozYevTJz7iZ6968OxGXSE4Ll3ePS027zQqFWZZUTnk"
}
```
### Example cURL
```
curl --location 'https://blueprint.bespot.io/api/auth/jwt/token/' \
--header 'Content-Type: application/json' \
--data '{ "username": "test-username", "password": "test-password" } '
```
### ACCESSING API RESOURCES
If the client is authenticated successfully, a JWT *access* token and a *refresh* token are returned. To consume an endpoint using JWT Authentication, the `Authorization` HTTP header should be filled with the acquired JWT *access* token prefixed with the following string: `Bearer `.
`Authorization: Bearer JWT_ACCESS_TOKEN`
### REFRESH JWT ACCESS TOKEN
JWT *access* tokens are expiring for security purposes. In such case, the JWT *access* token gets invalided and a new one should be acquired. To get a new JWT *access* token, client can use the provided *refresh* token from the `/auth/jwt/token/` response. See below the details of the authentication request.
### Resource \[POST]
`[/auth/jwt/token/refresh/]`
### Request
```
{
"refresh": "eyJhbGciOiJIUzI1NiJ9.eyJSb2xlIjoiQWRtaW4iLCJJc3N1ZXIiOiJJc3N1ZXIiLCJVc2VybmFtZSI6IkphdmFJblVzZSIsImV4cCI6MTcwODA3ODA1MiwiaWF0IjoxNzA4MDc4MDUyfQ.qozYevTJz7iZ6968OxGXSE4Ll3ePS027zQqFWZZUTnk"
}
```
### Response
```
{
"access": "eyJhbGciOiJIUzI1NiJ9.eyJSb2xlIjoiZ2VydGdlZmRzZGZzIiwiSXNzdWVyIjoiSXNzdWVyIiwiVXNlcm5hbWUiOiJkZnNkZmZkZnNmZHMiLCJleHAiOjE3MDgwNzkwNjUsImlhdCI6MTcwODA3OTA2NX0.SmcSysBMGLpUzgoBhyw7XjfLw4RYYuk7K5cAPcjdcxs",
"refresh": "eyJhbGciOiJIUzI1NiJ9.eyJSb2xlIjoiZ2VydGdlZmRzZGZzIiwiSXNzdWVyIjoiZHNmcmVncmdlciIsIlVzZXJuYW1lIjoiZGZzZGZmZGZzZmRzIiwiZXhwIjoxNzA4MDc5MDY1LCJpYXQiOjE3MDgwNzkwNjV9.IR93o7o3OfU5mSYts4i025D4rDMLPhkVe4UMhuZMzek"
}
```
### Example cURL
```
curl --location 'https://blueprint.bespot.io/api/auth/jwt/token/refresh/' \
--header 'Content-Type: application/json' \
--data '{
"refresh": "eyJhbGciOiJIUzI1NiJ9.eyJSb2xlIjoiQWRtaW4iLCJJc3N1ZXIiOiJJc3N1ZXIiLCJVc2VybmFtZSI6IkphdmFJblVzZSIsImV4cCI6MTcwODA3ODA1MiwiaWF0IjoxNzA4MDc4MDUyfQ.qozYevTJz7iZ6968OxGXSE4Ll3ePS027zQqFWZZUTnk"
}'
```
# Errors
Source: https://docs.bespot.com/blueprint/api/errors
API Errors
Bespot API uses HTTP status codes to communicate errors. A `code`, `detail`, `error_id` JSON structure is used. See below.
## 404 Not Found
```json theme={null}
{
"detail": "Store with this store_code not found.",
"code": "store_code_not_found",
"error_id": "2ac759de3d9b62d4"
}
```
## 401 Unauthorized
```json theme={null}
{
"detail": "Invalid username/password.",
"code": "authentication_failed",
"error_id": "6ebbc397d56be8e3"
}
```
# Server to server
Source: https://docs.bespot.com/blueprint/api/s2s
Here reside all endpoints available for Server2Server services.
## Store Verification
Store verification endpoint serves for validating the store where a device is present using EDDYSTONE Ephemeral Identifiers (EID) read from the installed BLE beacons. EID identifiers need to be provided (`readings` field) and the store (`store` field) will be returned if verification succeeds, else an error will be returned instead.
### Resource \[POST]
`[/stores/verify/]`
### Request
```
{
"readings": ["7ce21f44c8a30614","bc908667616da718","7ce21f44c8a30614"]
}
```
### Response
```
{
"store": "AHQ21000"
}
```
### Errors
Bespot API uses HTTP status code `400` to communicate errors. A `code`, `detail` JSON structure is used. See below.
#### Multiple stores
```
{
"error_code": "multiple_stores_for_eids",
"detail": "Multiple stores found (X) for given readings/EIDs."
}
```
#### No stores
```
{
"error_code": "no_stores_found",
"detail": "No store found matching the provided identifiers."
}
```
### Example cURL
```
curl --location --request POST 'https://blueprint.bespot.io/api/stores/verify/' \
--header 'Authorization: Basic afGeTREhfR72AkOlZqLGn4wBXvzVsHJyUI==' \
--header 'Content-Type: application/json' \
--data-raw '{
"readings": ["bfc0d982cf74aa4c","effab4e23b0b5cdc","adcb8179af7aeb9c"]
}'
```
# Analytics
Source: https://docs.bespot.com/blueprint/api/user-analytics
In this section, all endpoints available for Analytics services are described.
## Dwell Time
This resource returns the dwell time (the duration of a user's stay or visit) for a specific store and time interval. The response contains a list of results. Each result item represents a session.
> **Session**: A session starts (is created) at the moment the SDK consumer subscribes for InOut updates and closes when the SDK consumer unsubscribes. Typically, this occurs at application launch. In case the app goes to background, the session closes. When the app comes to foreground again, a new session starts. Finally, when the application is terminated abruptly, the session is never closed.
The following image depicts the different cases of filtering sessions by datetime.
### Resource \[GET]
`[/bi/dwelltime/]`
This resource supports pagination. Due to the increased data volume, adding pagination to this resource enables fetching data in batches (pages) over a specific time interval.
### URL Parameters
* `store_code` The store code - *Optional*
* `datetime_gte` An date string (in **UTC**) to be used as greater than or equal for filtering - *Required*
* `datetime_lte` An date string (in **UTC**) to be used as less than or equal for filtering - *Required*
* `device_id` The identifier of the user's device - *Optional*
* `page_size` The number of results in each page of the response - *Optional* Default value is used when param is not given
* `page` The number of the page of the response - *Optional* Needed only when the response is paginated
*`Note : Default page_size = 1000. Maximum page_size = 10000`*
### Response
The response is paginated. The `count` property shows the total results of the query. The `next` property contains the link of the subsequent page in case there is any - otherwise it has `null` value. Likewise with `previous` property. Finally, the `results` property contains the array of sessions either for a page or for the entire result. If `page_size` (default or set by the client) > `count` then only one page of results is returned and `next` and `previous` properties are `null`. To consume all pages, the client needs to continue fetching page-by-page using link provided within the `next` property until `null` value is found (last page - all sessions fetched).
In each result object, the following data are available:
* `session_id` The session identifier - *UUID*
* `store_code` The store code - *String*
* `device_id` The device identifier (filled by BespotSDK) - *UUID*
* `user_id` The user identifier (provided by the BespotSDK consumer) - *`UUID or empty string ("") if not provided`*
* `alt_user_id` An alternative user identifier (provided by the BespotSDK consumer) - *`String or empty string ("") if not provided`*
* `visit_start` The date and time the visit (or the session) started - *ISO 8601 Date and time with timezone*
* `visit_stop` The date and time the visit (or the session) ended - *ISO 8601 Date and time with timezone*
* `visit_duration` The duration of the visit (or the session) in seconds - *Integer*
```json theme={null}
{
"count": 6,
"next": "http://blueprint.bespot.io/api/bi/dwelltime/?datetime_gte=2021-04-14T09%3A00%3A00Z&datetime_lte=2021-04-14T11%3A30%3A00Z&page=2&page_size=3",
"previous": null,
"results": [
{
"session_id": "59a6c715-f934-4f62-84f7-1e987eb5d5ba",
"store_code": "AHQ21000",
"device_id": "4a1a5c0f-1523-4194-afbf-0d17cbc9257a",
"user_id": "31b3b029-0014-4702-bd65-d3323608f867",
"alt_user_id": "123456789",
"visit_start": "2021-05-12T12:40:53.570366+03:00",
"visit_stop": "2021-05-12T12:41:43.244950+03:00",
"visit_duration": 49
},
{
"session_id": "6f4ca126-df4f-428a-a205-e533c020d6a7",
"store_code": "AHQ21000",
"device_id": "4a1a5c0f-1523-4194-afbf-0d17cbc9257a",
"user_id": "31b3b029-0014-4702-bd65-d3323608f867",
"alt_user_id": "123456789",
"visit_start": "2021-05-12T12:42:34.167341+03:00",
"visit_stop": "2021-05-12T12:43:21.180259+03:00",
"visit_duration": 47
},
{
"session_id": "02dbc6bb-e50a-4220-84eb-00ad02864eda",
"store_code": "AHQ21000",
"device_id": "4a1a5c0f-1523-4194-afbf-0d17cbc9257a",
"user_id": "31b3b029-0014-4702-bd65-d3323608f867",
"alt_user_id": "123456789",
"visit_start": "2021-05-12T13:01:26.693398+03:00",
"visit_stop": null,
"visit_duration": null
}
]
}
```
[*`Note 1: Regarding the device_id value. In BespotSDK for Android it is created in every new application installation. In BespotSDK for iOS the identifierForVendor value is used - see more details here.`*](https://developer.apple.com/documentation/uikit/uidevice/1620059-identifierforvendor/)
*`Note 2: Last item in the array above shows an active session - not ended yet. Values in the visit_stop and visit_duration fields is null`*
### Example cURL
```
curl --location --request GET 'https://blueprint.bespot.io/api/bi/dwelltime/?store_code=AHQ21000&datetime_gte=2021-05-12T09:40:00Z&datetime_lte=2021-05-12T10:05:00Z' \
--header 'Authorization: Basic BASE64_ENCODED_CREDENTIALS'
```
***
## Unique users' count
This resource returns the unique users' count for a specific store and time interval.
### Resource \[GET]
`[/bi/unique-users-count/]`
### URL Parameters
* `store_code` The store code - *Optional*
* `datetime_gte` An date string (in **UTC**) to be used as greater than or equal for filtering - *Required*
* `datetime_lte` An date string (in **UTC**) to be used as less than or equal for filtering - *Required*
### Response
```json theme={null}
[
{
"name": "AthensHQ",
"store_code": "AHQ21000",
"count": 1
}
]
```
### Example cURL
```
curl --location --request GET 'https://blueprint.bespot.io/api/bi/dwelltime/?store_code=AHQ21000&datetime_gte=2021-03-18T09:00:00Z&datetime_lte=2021-03-18T12:00:00Z' \
--header 'Authorization: Basic BASE64_ENCODED_CREDENTIALS'
```
***
## Sessions' count
This resource returns the session count for a specific store and time interval.
### Resource \[GET]
`[/bi/sessions-count/]`
### URL Parameters
* `store_code` The store code - *Optional*
* `datetime_gte` An date string (in **UTC**) to be used as greater than or equal for filtering - *Required*
* `datetime_lte` An date string (in **UTC**) to be used as less than or equal for filtering - *Required*
### Response
```json theme={null}
[
{
"name": "AthensHQ",
"store_code": "AHQ21000",
"count": 3
}
]
```
### Example cURL
```
curl --location --request GET 'https://blueprint.bespot.io/api/bi/sessions-count/?store_code=AHQ21000&datetime_gte=2021-04-14T09:00:00Z&datetime_lte=2021-04-14T11:30:00Z' \
--header 'Authorization: Basic BASE64_ENCODED_CREDENTIALS'
```
# Blueprint Android SDK
Source: https://docs.bespot.com/blueprint/sdk/android
Install and configure the Android SDK for mobile applications.
The Blueprint SDK for Android leverages device sensors such as Bluetooth, WiFi, and Location to identify and locate a device in real-time within indoor environments. Furthermore, it provides proximity events and in-app content like surveys and notifications. See the User Analytics and Area Metrics endpoints for all available analytics.
## Get started
Requirements
* Android API 21+
# Bespot SDK Errors
Source: https://docs.bespot.com/blueprint/sdk/errors
SDK Errors
Bespot Android and iOS SDKs may report an error in case something goes wrong. Find the error list for each platform along with a short description in the following table.
| Android | iOS | Description |
| ---------------------------------------------- | ------------------------------------- | ------------------------------------------------------------------- |
| AuthFailure.NoActiveAccount | noActiveAccount | No active account found with the provided credentials. |
| SessionFailure.SessionNotFound | sessionNotFound | No active InOut session was found. |
| StatusFailure.IndoorDataModelNotFound | indoorDataModelNotFound | Indoor data model not found for store. |
| StatusFailure.InOutUnavailable | inOutUnavailable | InOut service is not available or returned an error. |
| StatusFailure.NoStoreReadings | noStoreReadings | No beacon readings found belonging to the current store. |
| StoreFailure.NoStoreNearby | noStoreNearby | No registered stores are found nearby. |
| StoreFailure.MultipleStoreNearby | multipleStores | Receiving beacon readings from multiple stores. |
| StoreFailure.NoStoreFound | noStoresFound | No store found matching the provided beacon identifiers. |
| StoreFailure.StoreUnderMaintenance | storeUnderMaintenance | The store is under maintenance. In/Out status is unavailable. |
| Failure.DeviceNotSupported | deviceNotSupported | Device not supported. In/Out status is unavailable. |
| Failure.NotInitialized | notInitialised | Bespot SDK is not initialised. |
| Failure.NetworkConnection | networkError | The internet is not reachable. |
| Failure.BluetoothPermissionDenied | bluetoothPermissionDenied | Bluetooth permission is denied. |
| Failure.BluetoothDisabled | bluetoothError | Bluetooth is powered off. |
| Failure.LocationPermissionDenied | - | Location Services permission is denied. |
| Failure.LocationServicesDisabled | - | Location Services are powered off. |
| Failure.ServerError | unknownServerError | An unknown server error occurred. |
| - | internalClientError | An internal BespotSDK client error occurred. |
| - | notAuthenticated | Authentication credentials were not provided. |
| - | notSubscribed | You first need to subscribe to receive In/Out updates. |
| - | noLocationorReadingsData | No location or beacon readings data provided to resolve to a store. |
| ServiceUnavailableFailure.OutsideBusinessHours | serviceUnavailableOutsideWorkingHours | Service is not available outside of business hours. |
# Blueprint iOS SDK
Source: https://docs.bespot.com/blueprint/sdk/ios
Install and configure the iOS SDK for mobile applications.
The Blueprint SDK for iOS leverages device sensors such as Bluetooth, WiFi, and Location to identify and locate a device in real-time within indoor environments. Furthermore, it provides proximity events and in-app content like surveys and notifications. See the User Analytics and Area Metrics endpoints for all available analytics.
## Get started
Requirements
* iOS 12.0+
* Xcode 14
# Blueprint SDK reference
Source: https://docs.bespot.com/blueprint/sdk/sdk
Overview of Bespot Blueprint SDKs across platforms.
Blueprint SDK is available for Android and iOS. We regularly update our SDK and API references to reflect the latest features and improvements. Our resources are designed to guide you in integrating and using Bespot Blueprint within your applications. Choose your preferred platform from the list below.
Native mobile applications
Native mobile applications
# Visitor Acquisition Metrics
Source: https://docs.bespot.com/datamine/api/acquisition-metrics
This section describes all endpoints available for People Counter Metrics.
> New data are available to these endpoints every 15 minutes.
***
## In, Out and Difference Metrics
This resource returns the number of visits at a store, the number of visits out of the store (exits) and the difference between these two numbers in *minute* granularity for the requested time interval.
### Resource \[GET]
`[/bi/acquisition-metrics/visits/]`
This resource supports pagination. Due to increased data volume, adding pagination to this resource allows fetching data in batches (pages) for a specific time interval.
### URL Parameters
* `store_code` The store code - *Optional*
* `datetime_gte` A date string (in **UTC**) to be used as greater than or equal for filtering - *Required*
* `datetime_lte` A date string (in **UTC**) to be used as less than or equal for filtering - *Required*
* `page_size` The number of results in each page of the response - *Optional* Default value is used when param is not given
* `page` The number of the page of the response - *Optional* Needed only when response is paginated
*`Note : Default page_size = 1000. Maximum page_size = 10000`*
### Response
The response is paginated. The `count` property shows the total results of the query. The `next` property contains the link of the subsequent page in case there is any - otherwise it has `null` value. Likewise with `previous` property. Finally, the `results` property contains the array of sessions either for a page or for the entire result. If `page_size` (default or set by the client) > `count` then only one page of results is returned and `next` and `previous` properties are `null`. In order to consume all pages, the client needs to continue fetching page-by-page using link provided within the `next` property until `null` value is found (last page - all sessions fetched).
In each result object, the following data are available:
* `store_code` The store code - *String*
* `in` Visits In the store - *Integer*
* `out` Visits Out of the store (exits) - *Integer*
* `diff` Difference. Visits In minus Visits Out - *Integer*
* `minute` The minute of measurement - *ISO 8601 Date and time with timezone*
```json theme={null}
{
"count": 6,
"next": "http://datamine.bespot.io/api/bi/acquisition-metrics/visits/?datetime_gte=2023-10-26T12%3A05%3A00Z&datetime_lte=2023-10-26T12%3A30%3A00Z&page=2&page_size=4&store_code=116500",
"previous": null,
"results": [
{
"store_code": "116500",
"in_metric": 3,
"out_metric": 0,
"diff_metric": 3,
"minute": "2023-10-26T15:06:00+03:00"
},
{
"store_code": "116500",
"in_metric": 0,
"out_metric": 1,
"diff_metric": -1,
"minute": "2023-10-26T15:08:00+03:00"
},
{
"store_code": "116500",
"in_metric": 1,
"out_metric": 2,
"diff_metric": -1,
"minute": "2023-10-26T15:09:00+03:00"
},
{
"store_code": "116500",
"in_metric": 0,
"out_metric": 2,
"diff_metric": -2,
"minute": "2023-10-26T15:10:00+03:00"
}
]
}
```
### Example cURL
```
curl --location 'https://datamine.bespot.io/api/bi/acquisition-metrics/visits/?datetime_gte=2023-10-26T12%3A05%3A00Z&datetime_lte=2023-10-26T12%3A30%3A00Z&store_code=116500&page_size=4' \
--header 'Authorization: Basic BASE64_ENCODED_CREDENTIALS'
```
***
## Occupancy Metric
This resource returns the number of visitors which are inside a store for a specific time interval in minute granularity.
### Resource \[GET]
`[/bi/acquisition-metrics/occupancy/]`
### URL Parameters
* `store_code` The store code - *Optional*
* `datetime_gte` A date string (in **UTC**) to be used as greater than or equal for filtering - *Required*
* `datetime_lte` A date string (in **UTC**) to be used as less than or equal for filtering - *Required*
* `page_size` The number of results in each page of the response - *Optional* Default value is used when param is not given
* `page` The number of page of the response - *Optional* Needed only when response is paginated
*`Note : Default page_size = 1000. Maximum page_size = 10000`*
### Response
In each result object, the following data are available:
* `store_code` The store code - *String*
* `occupancy_metric` Occupancy - *Integer*
* `minute` The minute of measurement - *ISO 8601 Date and time with timezone*
```json theme={null}
{
"count": 5,
"next": null,
"previous": null,
"results": [
{
"store_code": "116500",
"occupancy_metric": 7,
"minute": "2021-05-12T12:41:00+03:00",
},
{
"store_code": "116500",
"occupancy_metric": 9,
"minute": "2021-05-12T12:44:00+03:00",
},
{
"store_code": "116500",
"occupancy_metric": 12,
"minute": "2021-05-12T12:50:00+03:00",
},
{
"store_code": "116500",
"occupancy_metric": 11,
"minute": "2021-05-12T12:53:00+03:00",
},
{
"store_code": "116500",
"occupancy_metric": 11,
"minute": "2021-05-12T12:56:00+03:00",
}
]
}
```
### Example cURL
```
curl --location --request GET 'https://datamine.bespot.io/api/bi/acquisition-metrics/occupancy?store_code=116500&datetime_gte=2023-10-02T09:40:00Z&datetime_lte=2023-10-02T10:05:00Z' \
--header 'Authorization: Basic BASE64_ENCODED_CREDENTIALS'
```
# Errors
Source: https://docs.bespot.com/datamine/api/errors
API Errors
Bespot API uses HTTP status codes to communicate errors. A `code`, `detail`, `error_id` JSON structure is used. See below.
## 404 Not Found
```json theme={null}
{
"detail": "Store with this store_code not found.",
"code": "store_code_not_found",
"error_id": "2ac759de3d9b62d4"
}
```
## 401 Unauthorized
```json theme={null}
{
"detail": "Invalid username/password.",
"code": "authentication_failed",
"error_id": "6ebbc397d56be8e3"
}
```
# Visitor Retention Metrics
Source: https://docs.bespot.com/datamine/api/retention-metrics
This section describes endpoints available for People Counter Retention Metrics.
> New data are made available at 11:00 UTC on the following day.
***
## Retention metrics
This resource returns the following metrics in *day* granularity for the requested time interval:
| Metric | Description |
| -------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| Outside Traffic | The number of people who passed by the storefront area but did not end up visiting the store. |
| Turn in Coversion | The number of people who passed by the storefront area and entered the store within the predefined time range. |
| Average Visit Duration | The average duration that visitors spent within the store. |
| Returning Customers Last 14 Days | Visitors who had previously made a visit to the same store within the past 14 days. |
| New Customers Last 14 Days | Visitors who did not make any visits to the store in the past 14 days. |
| Returning Customers Last 60 Days | Visitors who had previously made a visit to the same store within the past 60 days. |
| New Customers Last 60 Days | Visitors who did not make any visits to the store in the past 60 days. |
**Notice: Deprecation of Metrics for GDPR Compliance**
Our provider has decided to discontinue the metrics Returning Customers Last 60 Days and New Customers Last 60 Days to align with GDPR regulations and uphold data privacy standards.
### Resource \[GET]
`[/bi/retention-metrics/]`
This resource supports pagination. Due to increased data volume, adding pagination to this resource allows fetching data in batches (pages) for a specific time interval.
### URL Parameters
* `store_code` The store code - *Optional*
* `datetime_gte` A date string (in **UTC**) to be used as greater than or equal for filtering - *Required*
* `datetime_lte` A date string (in **UTC**) to be used as less than or equal for filtering - *Required*
* `page_size` The number of results in each page of the response - *Optional* Default value is used when param is not given
* `page` The number of the page of the response - *Optional* Needed only when response is paginated
*`Note : Default page_size = 1000. Maximum page_size = 10000`*
### Response
The response is paginated. The `count` property shows the total results of the query. The `next` property contains the link of the subsequent page in case there is any - otherwise it has `null` value. Likewise with `previous` property. Finally, the `results` property contains the array of sessions either for a page or for the entire result. If `page_size` (default or set by the client) > `count` then only one page of results is returned and `next` and `previous` properties are `null`. In order to consume all pages, the client needs to continue fetching page-by-page using link provided within the `next` property until `null` value is found (last page - all sessions fetched).
In each result object, the following data are available:
* `store_code` The store code - *String*
* `outside_traffic` - *Integer*
* `turn_in_rate` - *Float*
* `avg_visit_duration` - *Float*
* `returning_cust_14` - *Float*
* `new_cust_14` - *Float*
* `returning_cust_60` - *Float*
* `new_cust_60` - *Float*
* `day` The day of measurement - *ISO 8601 Date and time with timezone*
```json theme={null}
{
"count": 5,
"next": "http://datamine.bespot.io/api/bi/retention-metrics/?datetime_gte=2023-11-13T00%3A00%3A00Z&datetime_lte=2023-11-18T23%3A59%3A00Z&page=2&page_size=3&store_code=116500",
"previous": null,
"results": [
{
"store_code": "116500",
"outside_traffic": 101,
"turn_in_rate": 0.7725,
"avg_visit_duration": 14.0,
"returning_cust_14": 0.84,
"new_cust_14": 0.16000000000000003,
"returning_cust_60": 0.92,
"new_cust_60": 0.07999999999999996,
"day": "2023-11-14T00:00:00+02:00"
},
{
"store_code": "116500",
"outside_traffic": 85,
"turn_in_rate": 0.8337,
"avg_visit_duration": 13.27,
"returning_cust_14": 0.75,
"new_cust_14": 0.25,
"returning_cust_60": 0.7916666666666666,
"new_cust_60": 0.20833333333333337,
"day": "2023-11-15T00:00:00+02:00"
},
{
"store_code": "116500",
"outside_traffic": 125,
"turn_in_rate": 0.7815000000000001,
"avg_visit_duration": 12.38,
"returning_cust_14": 0.5714285714285714,
"new_cust_14": 0.4285714285714286,
"returning_cust_60": 0.6285714285714286,
"new_cust_60": 0.37142857142857144,
"day": "2023-11-16T00:00:00+02:00"
}
]
}
```
### Example cURL
```
curl --location 'https://datamine.bespot.io/api/bi/retention-metrics?datetime_gte=2023-11-13T00%3A00%3A00Z&datetime_lte=2023-11-18T23%3A59%3A00Z&store_code=116500&page_size=3' \
--header 'Authorization: Basic BASE64_ENCODED_CREDENTIALS'
```
***
# Visitor Journey Tracking
Source: https://docs.bespot.com/datamine/api/visitor-journey-tracking
This endpoint returns zone-by-zone journey events for individual visits, including dwell time and zone transitions (entered from / exited to).
### Visit
A **visit** represents a single in-store presence for a device. A visit can include multiple zone events (e.g., Entrance → Produce → Dairy → Checkout).
### Session
A **session** aligns to a visit **per device**.
* A session begins when your Digital Asset (or Wi-Fi logs) is observed in-store and records activity.
* A session ends when activity stops, and no further activity is observed.
* **Multiple interruptions/reopens within a 60-minute window are consolidated** and billed as a single session.
### Zone event
A **zone event** is a segment of the journey where the visitor is observed within a single zone, with:
* entry context (`zone_entered_from`)
* exit context (`zone_exited_to`)
* dwell time (`dwell_time_seconds`)
## Resource \[GET]
`/bi/visit-journey-events/`
This resource supports pagination. Due to increased data volume, pagination allows fetching results in batches (pages) for a specified time interval.
***
## URL Parameters
* `store_code` — *Optional*\
Filter results to a single store.
* `datetime_gte` — *Required*\
Start datetime (UTC) inclusive. Format: ISO 8601.
* `datetime_lte` — *Required*\
End datetime (UTC) inclusive. Format: ISO 8601.
* `page_size` — *Optional*\
Number of results per page.\
**Default:** 1000, **Max:** 10000
* `page` — *Optional*\
Page number (used when paginating).
> **Note:** For large intervals, prefer daily windows (e.g., 24h) and paginate.
***
## Response
The response is paginated:
* `count` is the total number of results across the query.
* `next` and `previous` are URLs to fetch the adjacent pages.
* `results` is an array of **zone events**.
Each object in `results` contains:
* `session_id` — *String*\
Session identifier (aligned to “visit per device”, stitched within 60 minutes).
* `visit_id` — *String*\
Visit identifier (store visit per device).
* `user_id` — *String | null*\
User identifier (if available, e.g., logged-in app user). May be `null`.
* `device_id` — *String*\
Pseudonymous device identifier (privacy-safe token).
* `store_name` — *String*\
Store name.
* `store_code` — *String*\
Store code.
* `zone_name` — *String*\
Human-readable zone name.
* `zone_serial_code` — *String*\
Internal zone code used for stable referencing across systems.
* `zone_entered_from` — *String | null*\
Previous zone label. `null` if first event in the visit.
* `zone_exited_to` — *String | null*\
Next zone label. `null` if last event in the visit.
* `dwell_time_seconds` — *Integer*\
Time spent in the zone (seconds).
* `event_start` — *String*\
Zone event start timestamp (ISO 8601 UTC).
* `event_end` — *String*\
Zone event end timestamp (ISO 8601 UTC).
* `source` — *String*\
Source of observation, e.g. `app_ble`, `wifi`, `camera_fused`.
***
### Example response
```json theme={null}
{
"count": 6,
"next": null,
"previous": null,
"results": [
{
"session_id": "ses_2c7e41b9f0d84c7a",
"visit_id": "vst_8f2c9a1d3b7a4c1e",
"user_id": "usr_4a91d2c8",
"device_id": "dev_9b3f1c2a7e",
"date_of_visit": "2025-08-12",
"store_name": "SuperMart Downtown",
"store_code": "SM-001",
"zone_name": "Entrance & Promotions",
"zone_serial_code": "L0-ENT-PRO-001",
"zone_entered_from": "Outside",
"zone_exited_to": "Fresh Produce",
"dwell_time_seconds": 190,
"event_start": "2025-08-12T13:00:00Z",
"event_end": "2025-08-12T13:03:10Z",
"source": "app_sdk"
},
{
"session_id": "ses_2c7e41b9f0d84c7a",
"visit_id": "vst_8f2c9a1d3b7a4c1e",
"user_id": "usr_4a91d2c8",
"device_id": "dev_9b3f1c2a7e",
"date_of_visit": "2025-08-12",
"store_name": "SuperMart Downtown",
"store_code": "SM-001",
"zone_name": "Fresh Produce",
"zone_serial_code": "L0-PRD-FRU-014",
"zone_entered_from": "Entrance & Promotions",
"zone_exited_to": "Bakery",
"dwell_time_seconds": 540,
"event_start": "2025-08-12T13:03:10Z",
"event_end": "2025-08-12T13:12:10Z",
"source": "app_sdk"
},
{
"session_id": "ses_2c7e41b9f0d84c7a",
"visit_id": "vst_8f2c9a1d3b7a4c1e",
"user_id": "usr_4a91d2c8",
"device_id": "dev_9b3f1c2a7e",
"date_of_visit": "2025-08-12",
"store_name": "SuperMart Downtown",
"store_code": "SM-001",
"zone_name": "Bakery",
"zone_serial_code": "L0-BKY-BRD-006",
"zone_entered_from": "Fresh Produce",
"zone_exited_to": "Dairy",
"dwell_time_seconds": 310,
"event_start": "2025-08-12T13:12:10Z",
"event_end": "2025-08-12T13:17:20Z",
"source": "app_sdk"
},
{
"session_id": "ses_2c7e41b9f0d84c7a",
"visit_id": "vst_8f2c9a1d3b7a4c1e",
"user_id": "usr_4a91d2c8",
"device_id": "dev_9b3f1c2a7e",
"date_of_visit": "2025-08-12",
"store_name": "SuperMart Downtown",
"store_code": "SM-001",
"zone_name": "Dairy",
"zone_serial_code": "L0-DRY-MLK-022",
"zone_entered_from": "Bakery",
"zone_exited_to": "Household & Cleaning",
"dwell_time_seconds": 460,
"event_start": "2025-08-12T13:17:20Z",
"event_end": "2025-08-12T13:25:00Z",
"source": "app_sdk"
},
{
"session_id": "ses_2c7e41b9f0d84c7a",
"visit_id": "vst_8f2c9a1d3b7a4c1e",
"user_id": "usr_4a91d2c8",
"device_id": "dev_9b3f1c2a7e",
"date_of_visit": "2025-08-12",
"store_name": "SuperMart Downtown",
"store_code": "SM-001",
"zone_name": "Household & Cleaning",
"zone_serial_code": "L0-HOU-CLN-031",
"zone_entered_from": "Dairy",
"zone_exited_to": "Checkout Queue",
"dwell_time_seconds": 380,
"event_start": "2025-08-12T13:25:00Z",
"event_end": "2025-08-12T13:31:20Z",
"source": "app_sdk"
},
{
"session_id": "ses_2c7e41b9f0d84c7a",
"visit_id": "vst_8f2c9a1d3b7a4c1e",
"user_id": "usr_4a91d2c8",
"device_id": "dev_9b3f1c2a7e",
"date_of_visit": "2025-08-12",
"store_name": "SuperMart Downtown",
"store_code": "SM-001",
"zone_name": "Checkout Queue",
"zone_serial_code": "L0-CHK-QUE-001",
"zone_entered_from": "Household & Cleaning",
"zone_exited_to": "Cashlines",
"dwell_time_seconds": 260,
"event_start": "2025-08-12T13:31:20Z",
"event_end": "2025-08-12T13:35:40Z",
"source": "app_sdk"
}
]
}
```
## Notes and common questions
**Does one visit always equal one session?**\
For billing and analytics, we treat a **session as a visit per device**, with **60-minute stitching** to avoid multiple app opens during a single visit being counted multiple times.
\*\*Can \*\*`user_id`**be missing?** \
Yes. If the visitor is not logged in, or if the integration uses privacy-safe pseudonymous identifiers only, `user_id` may be `null`.
\*\*What does \*\*`source`**mean?** \
It indicates the signal that produced the event (e.g., Wi-Fi telemetry, app BLE, camera-derived, or fused).
# Authentication
Source: https://docs.bespot.com/datamine/auth
How to authenticate with the Datamine API
Bespot API supports both Basic Authentication and JWT Authentication over HTTPS. While both authentication methods are supported, API consumers are **strongly** advised to use JWT method to authenticate. To consume any resource the `Authorization` HTTP header is **required**.
## Basic Authentication
To consume an endpoint using Basic Authentication, the `Authorization` HTTP header should be filled with prefixed with the following string: `Basic ` and the available credentials encoded in *Base64* format.
`Authorization: Basic BASE64_ENCODED_CREDENTIALS`
## JWT Authentication
Before consuming any endpoint, each client needs to be authenticated with the API providing the authentication credentials. If the authentication is successful, a JWT `access` token is provided. This token can, then, be used in each subsequent API request so for the client to be authorised to access each the resource.
### GET JWT ACCESS TOKEN
See below the details of the authentication request to acquire the JWT *access* token.
### Resource \[POST]
`[/auth/jwt/token/]`
### Request
```
{
"username": "test-username",
"password": "test-password",
}
```
### Response
```
{
"access": "eyJhbGciOiJIUzI1NiJ9.eyJSb2xlIjoiZ2VydGdlIiwiSXNzdWVyIjoiZHNmIiwiVXNlcm5hbWUiOiJkZnNmZHMiLCJleHAiOjE3MDgwNzgwNTIsImlhdCI6MTcwODA3ODA1Mn0.QzC_fwqMsWUJS9VBiqLt_PJnQaCLnOI3B-xOEdK6Ops",
"refresh": "eyJhbGciOiJIUzI1NiJ9.eyJSb2xlIjoiQWRtaW4iLCJJc3N1ZXIiOiJJc3N1ZXIiLCJVc2VybmFtZSI6IkphdmFJblVzZSIsImV4cCI6MTcwODA3ODA1MiwiaWF0IjoxNzA4MDc4MDUyfQ.qozYevTJz7iZ6968OxGXSE4Ll3ePS027zQqFWZZUTnk"
}
```
### Example cURL
```
curl --location 'https://datamine.bespot.io/api/auth/jwt/token/' \
--header 'Content-Type: application/json' \
--data '{ "username": "test-username", "password": "test-password" } '
```
### ACCESSING API RESOURCES
If the client is authenticated successfully, a JWT *access* token and a *refresh* token are returned. To consume an endpoint using JWT Authentication, the `Authorization` HTTP header should be filled with the acquired JWT *access* token prefixed with the following string: `Bearer `.
`Authorization: Bearer JWT_ACCESS_TOKEN`
### REFRESH JWT ACCESS TOKEN
JWT *access* tokens are expiring for security purposes. In such case, the JWT *access* token gets invalided and a new one should be acquired. To get a new JWT *access* token, client can use the provided *refresh* token from the `/auth/jwt/token/` response. See below the details of the authentication request.
### Resource \[POST]
`[/auth/jwt/token/refresh/]`
### Request
```
{
"refresh": "eyJhbGciOiJIUzI1NiJ9.eyJSb2xlIjoiQWRtaW4iLCJJc3N1ZXIiOiJJc3N1ZXIiLCJVc2VybmFtZSI6IkphdmFJblVzZSIsImV4cCI6MTcwODA3ODA1MiwiaWF0IjoxNzA4MDc4MDUyfQ.qozYevTJz7iZ6968OxGXSE4Ll3ePS027zQqFWZZUTnk"
}
```
### Response
```
{
"access": "eyJhbGciOiJIUzI1NiJ9.eyJSb2xlIjoiZ2VydGdlZmRzZGZzIiwiSXNzdWVyIjoiSXNzdWVyIiwiVXNlcm5hbWUiOiJkZnNkZmZkZnNmZHMiLCJleHAiOjE3MDgwNzkwNjUsImlhdCI6MTcwODA3OTA2NX0.SmcSysBMGLpUzgoBhyw7XjfLw4RYYuk7K5cAPcjdcxs",
"refresh": "eyJhbGciOiJIUzI1NiJ9.eyJSb2xlIjoiZ2VydGdlZmRzZGZzIiwiSXNzdWVyIjoiZHNmcmVncmdlciIsIlVzZXJuYW1lIjoiZGZzZGZmZGZzZmRzIiwiZXhwIjoxNzA4MDc5MDY1LCJpYXQiOjE3MDgwNzkwNjV9.IR93o7o3OfU5mSYts4i025D4rDMLPhkVe4UMhuZMzek"
}
```
### Example cURL
```
curl --location 'https://datamine.bespot.io/api/auth/jwt/token/refresh/' \
--header 'Content-Type: application/json' \
--data '{
"refresh": "eyJhbGciOiJIUzI1NiJ9.eyJSb2xlIjoiQWRtaW4iLCJJc3N1ZXIiOiJJc3N1ZXIiLCJVc2VybmFtZSI6IkphdmFJblVzZSIsImV4cCI6MTcwODA3ODA1MiwiaWF0IjoxNzA4MDc4MDUyfQ.qozYevTJz7iZ6968OxGXSE4Ll3ePS027zQqFWZZUTnk"
}'
```
# Glossary
Source: https://docs.bespot.com/glossary
Key terms used in Bespot Gatekeeper and fraud prevention.
# Glossary
This glossary provides clear definitions of key terms used in Bespot Gatekeeper, offering insights into the concepts, technologies, and security mechanisms that drive our geolocation verification and fraud prevention solutions.
| **Term** | **Definition** |
| --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Account Sharing | The use of a single user ID across multiple devices or multiple user IDs on a single device. |
| API Server | The server that exposes Bespot's API endpoints for authentication, verification, fraud detection and data analytics. |
| App Tampering | Detect unauthorized modification of an app's code or stored data on the device/external memory. |
| App Version | Version of the client application. |
| Auth Server | The server that issues access tokens using OAuth 2.0’s Client Credentials Flow, enabling secure server-to-server verification and API access with JWT-based authentication. |
| Authentication | The process of verifying a user's identity using credentials such as API keys and JWT tokens. |
| Bearer Token | A type of access token used in HTTP authorization headers for secure API requests. |
| City | The city in which a transaction took place. |
| City Code | The city code for the city in which a transaction took place. |
| Client Server | A customer's backend system that communicates with Bespot's API Server for verification. |
| Cloned App | Detection of multiple instances of the same application running on a single device. |
| CocoaPods | A dependency management tool for Swift and Objective-C Cocoa projects, which simplifies the process of integrating third-party libraries. |
| Confidence Level | A measure of the likelihood that a user's reported location is accurate. |
| Connection Type | The network connection method used by a device (e.g., Wi-Fi, mobile data). |
| Country | The country in which a transaction took place. Reported in ISO 3166-1 alpha-2 format, as provided by Bespot’s geolocation and network analysis. |
| Dashboard | The Bespot Dashboard is a real-time monitoring interface tailored to risk and fraud teams for visualizing user transactions, evaluating policy outcomes, and tracking device activity. It also enables case management for investigating suspicious behavior. |
| Date | Timestamp when the transaction evaluation was performed. |
| Developer Options Menu | A default Android setting that enables advanced debugging and system modifications, often used by developers but exploitable for fraud. It allows location spoofing via mock location apps, aiding in reverse engineering, network interception, automated fraud, and device identity manipulation. Detecting Developer Mode usage helps prevent location-based fraud and unauthorized app modifications. |
| Developer Tools | A built-in browser feature for debugging, inspection, and network analysis. While essential for web development, it can be exploited for fraud through client-side code manipulation, location spoofing, API interception, and data extraction. Detecting its usage helps prevent web-based threats and unauthorized modifications. |
| Device Connection | Detect connection type, and carrier (mobile only). |
| Device Environment | Detect OS, version, locale, country, language. |
| Device Fingerprinting | A method of uniquely identifying a device based on various parameters like OS, manufacturer, and model. |
| Device ID | A system-generated unique device identifier created during the fingerprinting process, based on multiple device parameters. |
| Device Manufacturer | The brand or company that produced the device being used (e.g. Apple). |
| Device Model | The specific device model used for verification (e.g. iPhone 16 Pro Max). |
| Device Provenance | Detect device specifications, manufacturer and model details. |
| Display Mirroring | Detect display sharing to an external TV, monitor, or projector. |
| Emulator | Software that mimics a real device, is often used for developer testing or fraud attempts. |
| Encrypted Ticket | A cryptographically secured data structure that stores transaction verification results. It ensures data integrity and prevents tampering by allowing your client application to cross-check SDK responses, mitigating the risk of man-in-the-middle attacks. |
| Fraud Detection | The process of identifying and preventing suspicious or malicious activities in an application. |
| Gatekeeper SDK | A software development kit designed to protect applications from fraudulent activities by detecting and reporting potential threats using session, user and device information. |
| Geofence Presence | Verification of whether a user is within a predefined geographic boundary (geofence), enabling location-based access control and compliance enforcement. |
| Geofencing | The process of creating virtual boundaries (geofences) around a specific geographic area, such as a country, state, city, or building, to monitor and track a user’s location relative to the defined region. Geofences are configurable to suit specific location-based requirements and regulations. |
| Geolocation Compliance | Adherence to regulatory requirements that ensure users are accessing a service from an authorized location. |
| IP Address | A numerical identifier assigned to a device when it connects to a network, used to determine its origin and detect potential fraud. IP addresses can be analyzed for anomalies such as location mismatches, rapid changes, or association with VPNs and proxies. |
| IP Country | Device IP country information. |
| IP Geolocation | The method of estimating a device’s geographical location by analyzing its IP address. This process helps identify the country, region, city, and sometimes the internet service provider (ISP) associated with the connection. IP geolocation is commonly used for fraud detection, security monitoring, content restrictions, and compliance enforcement but can be affected by VPNs, proxies, and dynamic IP allocations. |
| JWT (JSON Web Token) | A secure token format is used for authentication and authorization in API interactions. |
| Latitude & Longitude | Reported geographical coordinates that pinpoint a device’s location. Latitude specifies north-south position, while longitude indicates east-west. |
| Location Accuracy | The deviation of reported location in meters indicates the precision of geolocation data. |
| Location Hopping | Detects improbable travel distances within short timeframes, signaling potential location spoofing or fraud attempts. |
| Location Insights | A dashboard feature that helps detect anomalies, such as clustering locations to identify fraud farms, unusual movement patterns, or inconsistencies in reported locations. |
| Location/IP Match | Detects discrepancies between IP geolocation and reported coordinates, ensuring regional consistency and identifying potential location spoofing. |
| Mock Location | Software-generated GPS coordinates used to spoof a device’s location and bypass geolocation checks. |
| Multiple Device IPs | Detects multiple IP addresses used by the same device, indicating possible network switching, VPN usage, or proxy masking. |
| Multiple User IPs | Detects a single User ID accessing an account from multiple IP addresses, which may indicate credential sharing, location spoofing, or compromised account activity. |
| Multiple Users on a Single Device | Detects multiple User IDs using the same Device UID, potentially signaling shared devices, fraudulent account farming, or compromised credentials. |
| OAuth 2.0 | An industry-standard protocol for secure API authorization, allowing applications to access resources without exposing user credentials. Bespot has adopted OAuth 2.0 for its reliability, security, and compatibility with modern authentication workflows. |
| OS | User’s device operating system (e.g. Android, iOS, Windows, MacOS). |
| OS Version | The version of the user’s device operating system. |
| Policy Action | The outcome of a transaction evaluation, determining whether to allow, monitor, limit, or block access based on a predefined fraud strategy. |
| Policy Actions | The outcome of a transaction evaluation, determined by the selected fraud strategy or custom rules. It defines whether an action is allowed, monitored, limited, or blocked based on risk assessment. |
| Proxy Connection | Detects active proxy server connections, which can be used to reroute traffic and obscure a user’s true IP address. |
| Proxy Detection | Detection of devices using proxy servers to mask their actual location. |
| Proxy Provider | Checks IP addresses against a database of known proxy providers to identify potential traffic anonymization or fraud attempts. |
| Region/State | The administrative region or state in which a transaction took place. |
| Reverse Geocoding | The process of converting geographic coordinates into readable location data (e.g., city, state, country). |
| RiskMap | A configurable geolocation feature that clusters unique device activity by area and time. It identifies activity hotspots to detect fraud patterns such as location farming, bots, or anomalies. The area is adjustable based on the use case for precise detection. |
| Root/Jailbreak Detection | The process of identifying unauthorized modifications to a device’s operating system (rooting on Android, jailbreaking on iOS). Such modifications can allow users to bypass security restrictions, potentially enabling GPS spoofing, app tampering, and other fraudulent activities. |
| Rule Detections | Fraud detection rules that were triggered during a transaction evaluation. Each detection corresponds to a specific risk factor, such as location spoofing, multiple device usage, or VPN/proxy masking. |
| SDK (Software Development Kit) | A collection of tools that allow developers to integrate Bespot’s geolocation and fraud detection features into their applications. |
| Server-to-Server Verification | A validation process where the client’s backend communicates directly with Bespot’s API server using an Encrypted Ticket. This cryptographically secured data structure ensures sensitive information is transmitted securely, preserving data integrity and preventing tampering or man-in-the-middle attacks. |
| Suspicious Activity | Any detected behavior that deviates from normal user activity patterns and may indicate fraud. Identification is based on Bespot’s customized or predefined industry-specific fraud prevention strategies. |
| Swift Package Manager (SPM) | A dependency management tool built into Swift that simplifies the integration and distribution of Swift libraries and frameworks. |
| Transactional Data | Logged information related to user interactions, including timestamps, user IDs, device details, and geolocation data. For a detailed breakdown of all transactional data fields, see Transactional Data Details. |
| User ID | A unique identifier for each user, as provided by the customer through the client application. |
| User Location | Collect device coordinates, accuracy, speed, and provider. |
| User Lookup | A feature in the dashboard that allows administrators to view and analyze individual user activity. |
| User on Multiple Devices | Detects when a single User ID is accessed from multiple devices, which may indicate credential sharing, unauthorized access, or account misuse. |
| Virtual Container | A method of isolating applications within a sandboxed environment, sometimes used for fraud. |
| VPN App Presence | Detects for installed known VPN applications on a device, which may indicate an attempt to manipulate location or bypass restrictions. |
| VPN Connection | Identifies active VPN connections, which may be used to mask a user's true location or bypass security measures. |
| VPN Detection | Detection of virtual private network (VPN) usage to mask a user’s true IP address. |
| VPN Provider | Checks IP addresses against a database of known VPN providers to detect anonymized or obfuscated traffic. |
# Info exchange flow
Source: https://docs.bespot.com/integration/info-exchange-flow
Data exchange flow between your systems and Bespot Gatekeeper APIs.
# Key Components
In this section, you’ll find a high-level overview of the components involved in integrating with Bespot Gatekeeper and how they exchange information. The following diagram offers a visual representation, and each component is detailed afterwards. By reviewing this overview first, you’ll gain the necessary context before integrating Bespot’s SDKs and services.
## Bespot Auth Server
The *Auth Server* issues access tokens based on the *Client Credentials Flow*, as defined in OAuth 2.0 (RFC 6749, Section 4.4). It is used in the server-to-server verification flow, granting the Client Server access to the API Server’s endpoints, specifically the `/api/s2s/verify` endpoint. The access tokens are in the form of JWTs (JSON Web Tokens) following the RFC 7519.
## Bespot API Server
The *API Server* exposes all available endpoints. Any entity that needs to consume these endpoints must be first authorized. The access token (JWT), previously acquired from the *Auth Server*, must be provided in the Authorization HTTP header with the `“Bearer ”` prefix for the resource to be accessible. Otherwise, a `401 Unauthorized` HTTP error is returned.
## Client Server
This is the customer’s *Client Server*. It receives verification requests from the customer’s frontend applications (Web, iOS, Android). These requests include the Action and Ticket information, which is then used as the body payload for the `/api/s2s/verify` HTTP endpoint of the API Server. Following successful verification of the Policy Action based on the *Ticket* information by the API Server, the *Client Server* informs the frontend applications that the verification is completed, allowing the transaction to proceed.
The Ticket is an encrypted string that can only be decrypted by the API Server and contains all information regarding the Detection Results, the *Policy Action*, and the timestamp information that the frontend applications initially received.
## Bespot Web SDK
The Web SDK incorporates functionality for performing detections on the *Web Browser*, communicating with Bespot’s *API Server*, and receiving the *Policy Action*. The Web SDK is the library to be used, as a dependency, in the customer’s application to provide the *Policy Action*.
## Bespot iOS SDK
The *iOS SDK* incorporates functionality for performing detections in iOS devices, communicating with Bespot’s *API Server* and receiving the *Policy Action*. The iOS SDK is the library to be used, as a dependency, in the customer’s iOS mobile application to provide the *Policy Action*.
## Bespot Android SDK
The *Android SDK* incorporates functionality for running checks in Android devices, communicating with Bespot’s *API Server* and receiving the *Policy Action*. The Android SDK is the library to be used, as a dependency, in the customer’s Android mobile application to provide the *Policy Action*.
## Client Web App
This is the customer's web application where Bespot’s *Web SDK* is used as a dependency and provides the *Policy Action*.
## Client iOS App
This is the customer's iOS application where Bespot’s *iOS SDK* is used as a dependency and provides the *Policy Action*.
## Client Android App
This is the customer's Android application where Bespot’s *Android SDK* is used as a dependency and provides the *Policy Action*.
# Integration overview
Source: https://docs.bespot.com/integration/integration
High-level overview of integrating Bespot Gatekeeper into your systems.
# Integration in 3 simple steps
Create and configure your account at [gatekeeper.bespot.com](https://gatekeeper.bespot.com)
Integrate our lightweight mobile and web SDKs via your preferred dependency manager
Enable MITM attack protection by establishing a secure communication between your client's server and the Bespot server, allowing 2-factor verification
# Integration planning
Source: https://docs.bespot.com/integration/integration-planning
Plan your integration in phases from account setup to production rollout.
# Integration Process
The solution integration consists of a comprehensive three-step process designed to ensure a seamless experience for developers.
## Phase 1: Account Configuration
During the initial account setup, an Organization is created to host the client's applications and Policy configurations. API keys for all required environments are generated at this stage. Rules and their associated checks are pre-configured on the Bespot API Server, with Policy Actions determined based on the specific business use case.
### Configure Environments
When a new Application is created and registered under the Organization, an API Key is issued. Separate applications can be created for different environments (e.g. Development, Staging, Testing, Production) and use separate API keys.
Policies and Rule configurations related to each Application are associated with the corresponding API key. Data between environments is logically separated.
For each *application*, the customer gets the following unique keys:
* API Key
* Client Id
* Client Secret
The above keys will be used in the following integration phases.
## Phase 2: SDK Integration
During this phase, the Software Development Kits (SDKs) are integrated into the client’s applications, enabling geolocation and fraud detection functionality across platforms (Web, Android, iOS).
Analytical technical guides on how to install and configure the Gatekeeper SDK (see [SDK](/sdk/web/) section) are available for each platform.
## Phase 3: Server-to-Server Integration (optional)
The final phase involves establishing secure communication between the client's server and the Bespot API server, allowing for verification actions and efficient data exchange.
In this final phase, a server-to-server integration should be done. Bespot *Gatekeeper API Server* needs to *verify* the *Policy Action* result received in the customer's frontend applications (Web, Android & iOS) to ensure the information has not been tampered with or altered in the meantime. This is done with the help of the *Client's Server* which collects the *Policy Action* result from the frontend applications and requests further verification from Bespot's *Gatekeeper API Server*. For more details on this flow, see API section -> Server2Server.
This structured approach delivers a fast integration that effectively meets the standards and needs of our customers. A dedicated team of technical, product, and account management experts provides real-time support throughout the integration phases, ensuring a smooth and efficient experience for our clients.
# Features
Source: https://docs.bespot.com/overview/features
Key product capabilities and detection features of Bespot Gatekeeper.
# Supported detections
The following table outlines the fraud detection capabilities available across platforms.
| **Detection** | **Description** | **Category** | **Web** | **Mobile** |
| --------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | ------------- | ------------- |
| Device identification | Generate a unique ID (seed) based on multiple parameters | Device Fingerprinting | Yes | Yes |
| Device provenance | Detect device specifications, manufacturer and model details | Device Fingerprinting | Yes | Yes |
| Device environment | Detect OS, version, locale, country, language | Device Fingerprinting | Yes | Yes |
| Device connection | Detect connection type and carrier (mobile only) | Device Fingerprinting | Yes | Yes |
| User location | Collect device coordinates, accuracy, speed, and provider | Location | Yes | Yes |
| Geofence presence | Verify user location inside or outside of multiple geofences | Location | Yes | Yes |
| Location hopping | Detect anomalies in reported locations of a session against plausible distance and time values (travelling modes supported) | Location | Yes | Yes |
| Location/IP match | IP geolocation matches with the coordinates (region-based) | Location | Yes | Yes |
| IP in country | IP geolocation matches the country in the allowlist | Location | Yes | Yes |
| Location permission | Location services permission granted by the user | Location | Yes | Yes |
| VPN connection | Check for established VPN connections | IP Anonymization | Yes | Yes |
| VPN provider | Check the IP address against known VPN providers | IP Anonymization | Yes | Yes |
| VPN app | Detect known VPN applications installed on the device | IP Anonymization | N/A | Yes |
| Proxy connection | Check for established Proxy server connections | IP Anonymization | Yes | Yes |
| Proxy provider | Check the IP address against known Proxy server providers | IP Anonymization | Yes | Yes |
| Multiple device IPs | Detects multiple IP addresses from the same device | IP Anonymization | Yes | Yes |
| User on multiple devices | A single User ID that is used on multiple devices on which the application runs | Account Sharing | Yes | Yes |
| Multiple users on a single device | Multiple User IDs that use a single Device UID to access each of the accounts | Account Sharing | Yes | Yes |
| Multiple user IPs | A single User ID connects with multiple IP addresses to access their account | Account Sharing | Yes | Yes |
| Developer Tools | Detect the use of browser developer tools | Device & Application Tampering | Yes | N/A |
| Developer Menu | Detect active developer menu (Android) | Device & Application Tampering | Beta | Yes |
| Root / Jailbreak | Detect device is rooted (Android) or jailbroken (iOS) | Device & Application Tampering | N/A | Yes |
| App Tampering | Detect unauthorized modification of an app's code or data | Device & Application Tampering | Yes | Yes |
| Display Mirroring | Detect display sharing to an external TV, monitor, or projector | Device & Application Tampering | Yes | Yes |
| Remote Desktop (RDP) | Detect remote desktop connections | Device & Application Tampering | *Coming Soon* | *Coming Soon* |
| Emulator | Detect emulation of mobile devices | Device & Application Tampering | N/A | Yes |
| Virtual Container | Detect virtual containers or sandboxing of applications | Device & Application Tampering | N/A | Yes |
| Cloned App | Detect multiple app instances on a single device | Device & Application Tampering | N/A | Yes |
### Notes
* **Geofence presence** on the Web SDK depends on the device's ability to report coordinates and the user granting the necessary permissions to their browser.
* **Geofence presence** can run on single or multiple geofences across geographies.
* **Location accuracy** is provided via API along with accuracy and confidence levels.
* **Reverse geocoding** is available for compliance reporting via API (city, state, country).
# Fraud types
Source: https://docs.bespot.com/overview/fraud-types
Common types of fraud that Bespot Gatekeeper helps detect and prevent.
# Common types of fraud
Below is a brief taxonomy of fraud scenarios. Gatekeeper can help mitigate or detect;
## Account Takeover / Credential Stuffing
Fraudsters gain unauthorized access to user accounts, often using leaked or stolen login details.
## Fake or Synthetic Accounts
Fraudsters create multiple or completely fabricated identities to exploit sign-up promos or bonuses.
## Payment Fraud
Use of stolen credit cards or payment instruments to fund accounts, purchase items, or launder funds.
## Chargeback Abuse / Friendly Fraud
Legitimate customers or fraudsters request chargebacks on valid transactions, leading to losses for merchants.
## Location Spoofing
Users mask or fake their physical location to circumvent regional restrictions or pricing.
## Account Sharing & Credential Sharing
Multiple people use a single subscription or betting account, reducing revenue and complicating compliance.
## Bot & Script Attacks
Automated software or scripts artificially generate transactions, ad clicks, or streaming plays to inflate metrics or siphon rewards.
# Who we protect
Source: https://docs.bespot.com/overview/who-we-protect
Industries and use cases that benefit from Bespot Gatekeeper.
# Industries
## Sports Betting & iGaming
Modern iGaming platforms often face fraudulent activities such as location spoofing to bypass regional restrictions and account takeover using stolen credentials. Fraudsters may create fake accounts to exploit promotions (e.g., sign-up bonuses), perform arbitrage, or engage in account sharing to pool resources and circumvent game rules. Gatekeeper helps operators comply with strict geolocation regulations, detect fraudulent behaviors in real time, and safeguard the betting experience by verifying user locations, monitoring devices for tampering, and enforcing your policies.
## Media Streaming & Content Delivery
In the streaming industry, fraud typically includes click farms or bot networks generating false views, imposter accounts uploading copyrighted content to siphon royalties, and account sharing to minimize subscription costs. Streaming providers also encounter geo-price manipulation, where users spoof their location to pay lower subscription fees. Gatekeeper combats these risks by detecting suspicious usage patterns (e.g., improbable geolocation data, rooted devices, mass logins), helping streaming platforms maintain genuine engagement metrics and protect rightful content owners.
## Financial Services
Banks and payment platforms confront complex threats such as payment fraud (stolen credit card data), chargeback abuse (consumers disputing valid transactions), and money laundering through rapid transactions or mismatched geolocation data. Gatekeeper’s device intelligence, network integrity checks, and real-time location validations help financial institutions reduce fraudulent transactions, ensure compliance with anti-money laundering (AML) mandates, and maintain user trust by verifying that customers truly are who—and where—they claim to be.
# Use Cases
* **Location-Based Compliance:** Verify user presence inside licensed regions (e.g., certain US states for iGaming or territory-specific streaming rights).
* **Device Integrity:** Detect rooting, jailbreaking, or app tampering that could facilitate malicious activity.
* **Network Fraud Detection:** Identify and flag usage over VPNs, Proxies, or suspicious IP addresses to prevent location spoofing or money laundering attempts.
* **Account Sharing & Multi-Device Tracking:** Monitor multiple logins from a single device or a single login from many devices, signalling possible account abuse or credential theft.
* **Promotion & Loyalty Abuse:** Prevent users from repeatedly redeeming new-customer promos or coupons by tracking device fingerprints and user session data.
# Gatekeeper Android SDK
Source: https://docs.bespot.com/sdk/android
Install and configure the Android SDK for mobile applications.
The Gatekeeper SDK for Android leverages device sensors and technologies combined with server-side checks to detect fraudulent activities in real-time and ensure the environment is safe for the mobile application to run.
## Get started
Requirements
* Android API 24+
# Gatekeeper Capacitor Plugins
Source: https://docs.bespot.com/sdk/capacitor
Install and configure our SDKs via Capacitor native runtime plugins
The Gatekeeper Capacitor Plugins leverage device sensors and technologies combined with server-side checks to detect fraudulent activities in real-time and ensure the environment is safe for applications to run.
## Get started
Requirements
* iOS 15.0+
* Xcode 16
Requirements
* Android API 24+
* Android Studio (latest stable)
# Gatekeeper iOS SDK
Source: https://docs.bespot.com/sdk/ios
Install and configure the iOS SDK for mobile applications.
The Gatekeeper SDK for iOS leverages device sensors and technologies combined with server-side checks to detect fraudulent activities in real-time and ensure the environment is safe for the mobile application to run.
## Get started
Requirements
* iOS 15.0+
* Xcode 15
# SDK reference
Source: https://docs.bespot.com/sdk/sdk
Overview of Bespot Gatekeeper SDKs across platforms.
Gatekeeper SDK is available for Web, Android and iOS. We regularly update our SDK and API references to reflect the latest features and improvements. Our resources are designed to guide you in integrating and using Bespot Gatekeeper within your applications. Choose your preferred platform from the list below.
Native mobile applications
Native mobile applications
Native web applications
Native runtime for web apps
# Gatekeeper Web SDK
Source: https://docs.bespot.com/sdk/web
Install and configure the Web SDK for browser-based applications.
The Gatekeeper SDK for Web leverages browser data and technologies combined with server-side checks to detect fraudulent activities in real-time and ensure the environment is safe for the web application to run.
## Get started
Supports all modern mobile and desktop web browsers.
Requirements (for npm installation):
* Node.js 20+