Webhooks
Receive an HTTP request to your server with Fingerprint results for every identification event the moment it happens.
Webhooks are a form of server-to-server communication allowing Fingerprint to send data to your application’s API endpoint every time we identify a visitor.
You can think of it as the reverse Server API — instead of having to request identification data, we send the identification events to your server as they happen. You can use webhooks to:
- Store all of your identification events in your database indefinitely. Fingerprint stores your events only for a limited time.
- Get Fingerprint results to your server securely and incorporate them into your fraud prevention logic or trigger other workflows for every identification event.
Webhooks are asynchronous and don't incur performance overhead when enabled. When multiple webhooks are enabled, events are delivered to each of them simultaneously.
Implementing a webhook handler
When you call the JavaScript agent get() method (or the mobile SDK equivalent), the identification request is processed by Fingerprint, and the device intelligence results are sent to your server endpoint.
To receive webhooks, you need to create an API endpoint in your server application that can receive POST HTTP requests from Fingerprint and return a successful response. For example:
import { VisitWebhook } from '@fingerprintjs/fingerprintjs-pro-server-api';
export async function POST(request: Request) {
const identificationEvent = (await request.json()) as VisitWebhook;
console.log(identificationEvent.visitorId);
fingerprintEventsDatabase.add(identificationEvent);
return Response.json({ message: 'Webhook received', status: 200 });
}
Note: Our Server SDKs can provide you with type information about the webhook format.
Once you add the webhook to your system, you can test it using cURL
:
curl <your-webhook-url> \
-X POST \
-H "Content-Type: application/json" \
-d '{ "tag": 2718281828, "visitorId": "3HNey93AkBW6CRbxV6xP", /* remaining fields here */}'
Timeout and errors
- Fingerprint expects your server endpoint to respond with a
2xx
status code within 3 seconds of receiving the webhook payload. - Otherwise, the webhook will be shown as Failed on the Webhook events page in the Dashboard.
- Headers and the response should be smaller than
4KB
, otherwise, they will be truncated.
Retries
- Every request is retried once if it times out or returns a non-
2xx
response. - The retry happens 5 minutes after the first unsuccessful request.
- The retry request will have the same request ID as the first request, so it's possible to use it as an idempotency key.
If you have multiple webhooks configured under one application, each request will be treated independently. If your webhook endpoint fails, a retry will only be triggered for the endpoint that caused the issue.
Registering your webhook URL
You can register webhooks from your Fingerprint account as follows:
- Navigate to Dashboard > Webhooks.
- Click Add webhook.
- Set URL to the HTTPS URL of your webhook endpoint. IP addresses or HTTP domains are not allowed.
- If you use Basic authentication to protect your webhook, fill in User and Password.
- Click Create Webhook.
- You will see a success modal that says Webhooks created.
- If you use Webhook signatures to verify the incoming webhooks (recommended), make sure to download or copy the encryption key shown on the success modal. The key is only shown once.
- Assuming your webhook handler is implemented and deployed, you can click Send test event to verify everything works as expected.
The created webhook is Enabled by default. You can manually deactivate your webhooks by clicking the Edit icon.
Webhook payload format
The webhook format is shared between all activated products. Fields can be specified as:
- optional (can be absent)
- nullable (can be null)
- empty (can be empty:
""
or{}
)
For example requestId
is not optional, it can't be null, it can't be empty.
Smart Signals Fields
The set of features exposed in the root object is determined by the pricing tier of your subscription.
suspectScore
,rootApps
,tampering
,proxy
,vpn
,tor
,ipBlocklist
,bot
,ipInfo
,emulator
,clonedApp
,factoryReset
,jailbroken
,frida
,privacySettings
,virtualMachine
,remoteTools
,velocity
,developerTools
andrawDeviceAttributes
fields are unavailable on the basic Pro tier.The webhook format exposes the same Smart Signals information as the Server API
/events
endpoint, even though the format is not exactly the same. The response payload is based on the originating platform (browser, iOS SDK, Android SDK) and feature availability. See our Smart Signals cheat sheet for a complete overview.
JSON example
{
// Unique request identifier
// maxLength: 20
"requestId": "1708102555327.NLOjmg",
// customer-provided data, for example requestType and yourCustomId
// both the tag and the information it contains are optional
// and only for the customer's need.
// empty, maxLength: 16KB
"tag": {
"requestType": "signup",
"yourCustomId": 45321
},
// user-provided scalar identifier
// optional,maxLength: 256
"linkedId": "any-string",
// persistent visitor identifier
// helpful to detect anonymous or private mode users
// maxLength: 20
"visitorId": "3HNey93AkBW6CRbxV6xP",
// tells whether a visitor was found in a visits history
"visitorFound": true,
// timestamp in milliseconds (since unix epoch)
//
"timestamp": 1554910997788,
// time in RFC3339 format
//
"time": "2019-04-10T15:43:17.788Z",
// if the page view was made in incognito or private mode
"incognito": false,
// URL where the API was called in the browser
// empty maxLength: 4096
"url": "https://banking.example.com/signup",
// The URL of a client-side referrer
// optional, maxLength: 4096
"clientReferrer": "https://google.com?search=banking+services",
// maxLength: 40
"ip": "216.3.128.12",
// empty
// This field is deprecated and will not return a result for applications created after January 23rd, 2024
// See https://dev.fingerprint.com/docs/smart-signals-overview#ip-geolocation for a replacement in our Smart Signals product
"ipLocation": {
// kilometers
// optional
"accuracyRadius": 1,
// optional
"city": {
// empty, maxLength: 4096
"name": "Bolingbrook"
},
// optional
"continent": {
// empty, maxLength: 2
"code": "NA",
// empty, maxLength: 40
"name": "North America"
},
// optional
"country": {
// empty, maxLength: 2
"code": "US",
// empty, maxLength: 250
"name": "United States"
},
// optional
"latitude": 41.12933,
// optional
"longitude": -88.9954,
// optional, maxLength: 40
"postalCode": "60547",
// optional
"subdivisions": [
{
// empty, maxLength: 250
"isoCode": "IL",
// empty, maxLength: 250
"name": "Illinois"
}
],
// optional, maxLength: 250
"timezone": "America/Chicago"
},
"browserDetails": {
// empty, maxLength: 250
"browserName": "Chrome",
// empty, maxLength: 250
"browserFullVersion": "73.0.3683.86",
// empty, maxLength: 250
"browserMajorVersion": "73",
// empty, maxLength: 250
"os": "Mac OS X",
// empty, maxLength: 250
"osVersion": "10.14.3",
// empty, maxLength: 250
"device": "Other",
// empty, maxLength: 4096
"userAgent": "(Macintosh; Intel Mac OS X 10_14_3) Chrome/73.0.3683.86"
},
// optional
"confidence": {
// 0 - 1
"score": 0.97
},
"firstSeenAt": {
// nullable
"global": "2022-03-16T11:26:45.362Z",
// nullable
"subscription": "2022-03-16T11:31:01.101Z"
},
"lastSeenAt": {
// nullable
"global": "2022-03-16T11:28:34.023Z",
// nullable
"subscription": null
},
// optional, maxLength: 4096
"userAgent": "(Macintosh; Intel Mac OS X 10_14_3) Chrome/73.0.3683.86"
}
{
// Unique request identifier
// maxLength: 20
"requestId": "1708102555327.NLOjmg",
// customer-provided data, for example requestType and yourCustomId
// both the tag and the information it contains are optional
// and only for the customer's need.
// empty, maxLength: 16KB
"tag": {
"requestType": "signup",
"yourCustomId": 45321
},
// user-provided scalar identifier
// optional,maxLength: 256
"linkedId": "any-string",
// persistent visitor identifier
// helpful to detect anonymous or private mode users
// maxLength: 20
"visitorId": "3HNey93AkBW6CRbxV6xP",
// tells whether a visitor was found in a visits history
"visitorFound": true,
// timestamp in milliseconds (since unix epoch)
//
"timestamp": 1554910997788,
// time in RFC3339 format
//
"time": "2019-10-12T07:20:50.52Z",
// if the page view was made in incognito or private mode
"incognito": false,
// URL where the API was called in the browser
// empty maxLength: 4096
"url": "https://banking.example.com/signup",
// The URL of a client-side referrer
// optional, maxLength: 4096
"clientReferrer": "https://google.com?search=banking+services",
// maxLength: 40
"ip": "216.3.128.12",
// empty
// This field is deprecated and will not return a result for applications created after January 23rd, 2024
// See https://dev.fingerprint.com/docs/smart-signals-overview#ip-geolocation for a replacement in our Smart Signals product
"ipLocation": {
// kilometers
// optional
"accuracyRadius": 1,
// optional
"city": {
// empty, maxLength: 4096
"name": "Bolingbrook"
},
// optional
"continent": {
// empty, maxLength: 2
"code": "NA",
// empty, maxLength: 40
"name": "North America"
},
// optional
"country": {
// empty, maxLength: 2
"code": "US",
// empty, maxLength: 250
"name": "United States"
},
// optional
"latitude": 41.12933,
// optional
"longitude": -88.9954,
// optional, maxLength: 40
"postalCode": "60547",
// optional
"subdivisions": [
{
// empty, maxLength: 250
"isoCode": "IL",
// empty, maxLength: 250
"name": "Illinois"
}
],
// optional, maxLength: 250
"timezone": "America/Chicago"
},
"browserDetails": {
// empty, maxLength: 250
"browserName": "Chrome",
// empty, maxLength: 250
"browserFullVersion": "73.0.3683.86",
// empty, maxLength: 250
"browserMajorVersion": "73",
// empty, maxLength: 250
"os": "Mac OS X",
// empty, maxLength: 250
"osVersion": "10.14.3",
// empty, maxLength: 250
"device": "Other",
// empty, maxLength: 4096
"userAgent": "(Macintosh; Intel Mac OS X 10_14_3) Chrome/73.0.3683.86"
},
// optional
"confidence": {
// 0 - 1
"score": 0.97
},
"firstSeenAt": {
// nullable
"global": "2022-03-16T11:26:45.362Z",
// nullable
"subscription": "2022-03-16T11:31:01.101Z"
},
"lastSeenAt": {
// nullable
"global": "2022-03-16T11:28:34.023Z",
// nullable
"subscription": null
},
// optional, maxLength: 4096
"userAgent": "(Macintosh; Intel Mac OS X 10_14_3) Chrome/73.0.3683.86",
// optional, empty
"ipInfo": {
"v4": {
"address": "94.142.239.124",
"geolocation": {
"accuracyRadius": 20,
"latitude": 50.05,
"longitude": 14.4,
"postalCode": "150 00",
"timezone": "Europe/Prague",
"city": {
"name": "Prague"
},
"country": {
"code": "CZ",
"name": "Czechia"
},
"continent": {
"code": "EU",
"name": "Europe"
},
"subdivisions": [
{
"isoCode": "10",
"name": "Hlavni mesto Praha"
}
]
}
},
// optional, empty
"asn": {
// optional, empty
"asn": "7922",
// optional, empty
"name": "COMCAST-7922",
// optional, empty
"network": "73.136.0.0/13"
},
"dataCenter": {
"result": true,
// optional, empty
"name": "DediPath"
}
},
// optional, empty
"bot": {
// "good" | "bad" | "notDetected"
// See more info at https://dev.fingerprint.com/docs/bot-detection-quick-start-guide
"result": "notDetected"
},
// optional, empty
"vpn": {
"result": false,
"methods": {
"timezoneMismatch": false,
"publicVPN": false
}
},
// optional, empty
"tampering": {
"result": false,
"anomalyScore": 0
},
// Indicates if anti-fingerprinting settings are enabled for Firefox and Brave browsers
// optional, empty
"privacySettings": {
"result": false
},
// Indicates if the request was initiated from a virtual machine
// optional, empty
"virtualMachine": {
"result": false
},
// optional, empty
"remoteControl": {
"result": false
},
// optional, empty
"velocity": {
"distinctIp": {
"intervals": {
"5m": 1,
"1h": 1,
"24h": 1
}
},
"distinctLinkedId": {},
"distinctCountry": {
"intervals": {
"5m": 1,
"1h": 1,
"24h": 1
}
},
"events": {
"intervals": {
"5m": 1,
"1h": 1,
"24h": 1
}
}
},
// optional, empty
"developerTools": {
"result": false
},
// Returns Suspect Score computed from other Smart Signals
// See https://dev.fingerprint.com/docs/suspect-score
// optional, empty
"suspectScore": {
"result": 0
}
}
{
// Unique request identifier
// maxLength: 20
"requestId": "1708102555327.NLOjmg",
// customer-provided data, for example requestType and yourCustomId
// both the tag and the information it contains are optional
// and only for the customer's need.
// empty, maxLength: 16KB
"tag": {
"requestType": "signup",
"yourCustomId": 45321
},
// user-provided scalar identifier
// optional,maxLength: 256
"linkedId": "any-string",
// persistent visitor identifier
// helpful to detect anonymous or private mode users
// maxLength: 20
"visitorId": "3HNey93AkBW6CRbxV6xP",
// tells whether a visitor was found in a visits history
"visitorFound": true,
// timestamp in milliseconds (since unix epoch)
//
"timestamp": 1554910997788,
// time in RFC3339 format
//
"time": "2019-10-12T07:20:50.52Z",
// if the page view was made in incognito or private mode
"incognito": false,
// URL where the API was called in the browser
// empty maxLength: 4096
"url": "https://banking.example.com/signup",
// The URL of a client-side referrer
// optional, maxLength: 4096
"clientReferrer": "https://google.com?search=banking+services",
// maxLength: 40
"ip": "216.3.128.12",
// empty
// This field is deprecated and will not return a result for applications created after January 23rd, 2024
// See https://dev.fingerprint.com/docs/smart-signals-overview#ip-geolocation for a replacement in our Smart Signals product
"ipLocation": {
// kilometers
// optional
"accuracyRadius": 1,
// optional
"city": {
// empty, maxLength: 4096
"name": "Bolingbrook"
},
// optional
"continent": {
// empty, maxLength: 2
"code": "NA",
// empty, maxLength: 40
"name": "North America"
},
// optional
"country": {
// empty, maxLength: 2
"code": "US",
// empty, maxLength: 250
"name": "United States"
},
// optional
"latitude": 41.12933,
// optional
"longitude": -88.9954,
// optional, maxLength: 40
"postalCode": "60547",
// optional
"subdivisions": [
{
// empty, maxLength: 250
"isoCode": "IL",
// empty, maxLength: 250
"name": "Illinois"
}
],
// optional, maxLength: 250
"timezone": "America/Chicago"
},
"browserDetails": {
// empty, maxLength: 250
"browserName": "Chrome",
// empty, maxLength: 250
"browserFullVersion": "73.0.3683.86",
// empty, maxLength: 250
"browserMajorVersion": "73",
// empty, maxLength: 250
"os": "Mac OS X",
// empty, maxLength: 250
"osVersion": "10.14.3",
// empty, maxLength: 250
"device": "Other",
// empty, maxLength: 4096
"userAgent": "(Macintosh; Intel Mac OS X 10_14_3) Chrome/73.0.3683.86"
},
// optional
"confidence": {
// 0 - 1
"score": 0.97
},
"firstSeenAt": {
// nullable
"global": "2022-03-16T11:26:45.362Z",
// nullable
"subscription": "2022-03-16T11:31:01.101Z"
},
"lastSeenAt": {
// nullable
"global": "2022-03-16T11:28:34.023Z",
// nullable
"subscription": null
},
// optional, maxLength: 4096
"userAgent": "(Macintosh; Intel Mac OS X 10_14_3) Chrome/73.0.3683.86",
// optional, empty
"ipInfo": {
"v4": {
"address": "94.142.239.124",
"geolocation": {
"accuracyRadius": 20,
"latitude": 50.05,
"longitude": 14.4,
"postalCode": "150 00",
"timezone": "Europe/Prague",
"city": {
"name": "Prague"
},
"country": {
"code": "CZ",
"name": "Czechia"
},
"continent": {
"code": "EU",
"name": "Europe"
},
"subdivisions": [
{
"isoCode": "10",
"name": "Hlavni mesto Praha"
}
]
},
// optional, empty
"asn": {
// optional, empty
"asn": "7922",
// optional, empty
"name": "COMCAST-7922",
// optional, empty
"network": "73.136.0.0/13"
},
"dataCenter": {
"result": true,
// optional, empty
"name": "DediPath"
}
}
},
// optional, empty
"bot": {
// "good" | "bad" | "notDetected"
// See more info at https://dev.fingerprint.com/docs/bot-detection-quick-start-guide
"result": "notDetected"
},
// optional, empty
"rootApps": {
"result": false
},
// optional, empty
"emulator": {
"result": false
},
// optional, empty
"ipBlocklist": {
"result": false,
"details": {
"emailSpam": false,
"attackSource": false
}
},
// optional, empty
"tor": {
"result": false
},
// optional, empty
"vpn": {
"result": false,
"methods": {
"timezoneMismatch": false,
"publicVPN": false
}
},
// optional, empty
"proxy": {
"result": false
},
// optional, empty
"tampering": {
"result": false,
"anomalyScore": 0
},
// Indicates if the request was initiated from a cloned app. Only for Android SDK.
// optional, empty
"clonedApp": {
"result": false
},
// Timestamp of when a factory reset is likely to have happened. Only for Mobile SDKs.
// optional, empty
"factoryReset": {
"time": "1970-01-01T00:00:00Z",
"timestamp": 0
},
// Indicates if the device has been jailbroken. Only for iOS SDK.
// optional, empty
"jailbroken": {
"result": false
},
// Indicates if frida could have been used to tamper with the app. Only for Mobile SDKs.
// optional, empty
"frida": {
"result": false
},
// Indicates if anti-fingerprinting settings are enabled for Firefox and Brave browsers
// optional, empty
"privacySettings": {
"result": false
},
// Indicates if the request was initiated from a virtual machine
// optional, empty
"virtualMachine": {
"result": false
},
// optional, empty
"remoteControl": {
"result": false
},
// optional, empty
"velocity": {
"distinctIp": {
"intervals": {
"5m": 1,
"1h": 1,
"24h": 1
}
},
"distinctLinkedId": {},
"distinctCountry": {
"intervals": {
"5m": 1,
"1h": 1,
"24h": 1
}
},
"events": {
"intervals": {
"5m": 1,
"1h": 1,
"24h": 1
}
}
},
// optional, empty
"developerTools": {
"result": false
},
// Raw attributes that were collected from the client's browser and that were used by
// Fingerprint to perform identification.
// optional, empty
"rawDeviceAttributes": {
// Visit https://dev.fingerprint.com/reference/getevent to learn more about the
// 'rawDeviceAttributes' object and all the possible values that it can contain.
},
// Returns Suspect Score computed from other Smart Signals
// See https://dev.fingerprint.com/docs/suspect-score
// optional, empty
"suspectScore": {
"result": 0
}
}
Our Server SDKs can also provide you with type information about the webhook format.
For an explanation of the individual Smart Signals fields see the Smart Signals overview. For more information about timestamps, see Visitor timestamps.
Protecting your webhooks
There are three possible approaches to webhook security: Secret URLs, Basic Authentication, and Webhook signatures.
Secret URL
You can make your Webhook URL impossible to guess and keep it secret. This is the most basic form of protection. It doesn't secure the URL itself, but as long as the URL does not leak, no one can send fake identification events to it.
To ensure your data is encrypted, we require using HTTPS for all webhook communication.
Basic HTTP Authentication
An easy way to protect your API is through Basic HTTP Authentication. You can configure your web server to require a username and password to access a URL.
To enable Basic authentication for your webhook:
- Navigate to Webhooks.
- Find your webhook in the table and click the Edit icon.
- Expand Basic authentication.
- Fill in User and Password.
- Click Edit webhook.
Webhook signatures
Enterprise only
Webhook header signatures are available only on the Enterprise plan.
Each webhook request contains a special FPJS-Event-Signature
HTTP header that is available as an additional validation measure. We strongly suggest adopting this method instead of Basic HTTP Authentication as it doesn't require sharing any kind of sensitive data (username/password).
- The header signature is computed from the HTTP body of the webhook along with a symmetric key — a secret generated during the webhook creation.
- The signature method used is HMAC with SHA-256 hash function.
Since the secret remains unique to a specific webhook, it's impossible to spoof invalid data without it.
FPJS-Event-Signature
Header Structure
FPJS-Event-Signature
Header StructureThe value of the FPJS-Event-Signature
header is a comma-separated list of <signatureversion>=<hash>
where the signatureversion
is the version of the signature and hash
is the computed signature hash.
Currently, the only supported version is v1
and the algorithm for v1
is a HMAC SHA-256 of the HTTP raw payload, using the webhook's secret. The secret is generated for each webhook during its creation.
1. Get the Key from the Dashboard
When you add a webhook, we assign a unique symmetric key to it and display it once. Save this secret for subsequent usage on your backend so that you can compute your HMAC and validate the webhook from the signature.
2. Verify Signature
The HMAC signature we assign to the FPJS-Event-Signature
header is calculated from the raw HTTP payload of the request. An example validation code might look like this:
import * as crypto from "crypto";
import { Buffer } from 'buffer';
const checkSignature = (signature: string, data: Buffer, secret: string) => {
return signature === crypto.createHmac('sha256', secret).update(data).digest('hex');
}
const checkHeader = (header: string, data: Buffer, secret: string) => {
const signatures = header.split(',');
for (const signature of signatures) {
const [version, hash] = signature.split('=');
if (version === 'v1') {
if (checkSignature(hash, data, secret)) {
return true;
}
}
}
return false
}
//valid (allow)
console.log(checkHeader('v1=89e14bbd118da7945e4547c1b9f32fff890dc141a7162df45c1ccb7546a80b58', Buffer.from('payload'), 'secret'))
//invalid (reject)
console.log(checkHeader('v0=89e14bbd118da7945e4547c1b9f32fff890dc141a7162df45c1ccb7546a80b58', Buffer.from('payload'), 'wrongsecret'))
console.log(checkHeader('v1=79e14bbd118da7945e4547c1b9f32fff890dc141a7162df45c1ccb7546a80b58', Buffer.from('payload'), 'wrongsecret'))
import { isValidWebhookSignature } from '@fingerprintjs/fingerprintjs-pro-server-api'
/**
* Webhook endpoint handler example
* @param {Request} request
*/
export async function POST(request) {
try {
const secret = process.env.WEBHOOK_SIGNATURE_SECRET
const header = request.headers.get('fpjs-event-signature')
const data = Buffer.from(await request.arrayBuffer())
if (!secret) {
return Response.json({ message: 'Secret is not set.' }, { status: 500 })
}
if (!header) {
return Response.json({ message: 'fpjs-event-signature header not found.' }, { status: 400 })
}
if (!isValidWebhookSignature({ header, data, secret })) {
return Response.json({ message: 'Webhook signature is invalid.' }, { status: 403 })
}
return Response.json({ message: 'Webhook received.' }, { status: 200 })
} catch (error) {
return Response.json({ error }, { status: 500 })
}
}
Key Rotation
To rotate the webhook key:
- Create another Webhook with the same URL and a distinctive name.
- Rotate the key on your backend.
- Once you start ingesting the new signature, delete the old webhook completely to prevent duplicated information.
Updated about 1 month ago