Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.fingerprint.com/llms.txt

Use this file to discover all available pages before exploring further.

  • Significant increase in accuracy in browsers with strict privacy features such as Safari or Firefox.
  • Cookies are now recognized as “first-party.” This means they can live longer in the browser and extend the lifetime of visitor IDs.
  • Ad blockers, browser extensions, and VPNs will not block the Fingerprint client agent from loading and making identification requests. Connecting to a Fingerprint CDN and API is blocked by most ad blockers but connecting to the same site URL is allowed. To learn more, see Protecting the JavaScript agent from ad blockers.
Fingerprint offers a variety of off-the-shelf proxying solutions. These might not work for you if you cannot easily edit your DNS records, all of your production code must run on your servers, or other technical constraints specific to your business. If you need to develop a custom proxy integration, this article will guide you through.
This documentation applies to the JavaScript agent v4. For the deprecated v3 version, see v3 custom proxy integrations. If you are migrating from v3 to v4, you can see an overview of the required changes in the migration guide below.

Existing proxy integrations

Fingerprint currently offers these ready-made proxy integrations: These integrations are developed, tested, documented, maintained, and officially supported by Fingerprint. They are deployed in production by numerous Fingerprint customers. If you can use any of these integrations, it is highly recommended to do so. If you are missing an official integration for your platform, please submit an idea to the Fingerprint Product Roadmap.

Custom proxy integration requirements

Your proxy integration will consist of several server-side API endpoints that proxy requests from Fingerprint client libraries to the Fingerprint servers. Your proxy logic must forward the HTTP request payload and correctly handle request headers and query parameters of each request to conform to the Fingerprint API data contract.
Limitations and expectations

Limited to Enterprise planCustom Fingerprint Proxy Integrations are exclusively supported for customers on the Enterprise Plan. Other customers are encouraged to use the Custom subdomain setup or the Cloudflare Proxy Integration.

Updates occasionally requiredThe underlying data contract in the identification logic can change to keep up with browser and device releases. Using a custom proxy integration might require occasional updates to your proxy integration code. Ignoring these updates will lead to lower accuracy or service disruption.

First-party context

Your integration must be served in a first-party context. It must be available on a URL on the main website domain or one of its subdomains.
  • For example, if your website is yourwebsite.com, then yourwebsite.com/metrics or metrics.yourwebsite.com are both acceptable URLs.
  • When choosing the endpoint URLs, don’t use fingerprint, fpjs, or other words related to fingerprinting that might get blocked by ad blockers. Pick generic words like metrics, data, or completely random strings.

Types of HTTP requests to proxy

A proxy integration sits between the Fingerprint client agent on the browser/device and Fingerprint servers. There are three kinds of HTTP requests that the client agent makes:
HTTP requestHTTP MethodThe purpose of a proxy integration
Agent download request (browser only) — downloads the latest device intelligence logic from the Fingerprint CDNGETavoid ad blockers, drop cookies, handle caching
Browser cache request (browser only) — gathers additional signalsGETavoid ad blockers, drop cookies, handle caching
Identification request — sends collected signals to Fingerprint, gets the identification result in returnPOSTavoid ad blockers, drop cookies
The requirements for proxying each request are described in detail below. Please note that the TypeScript snippets are provided for demonstration purposes only. They are not meant to be a fully working copy-pastable solution. Some are missing implementation details that vary depending on your setup. Adapt the examples for your language, server stack, and infrastructure environment.

1. Proxy the Agent download request (browsers only)

  1. Create a GET server endpoint. Make it available on a /RANDOM_PATH/web/* path in your application, so that it will expect additional path segments. The random path segment is used to avoid detection by ad blockers.
    // Request handler for the agent-download request
    // Available on yourwebsite.com/RANDOM_PATH/web/*
    
    export async function GET(request: Request) {
      // Agent download proxy implementation
    }
    
  2. Use the path segment after web to compile correct agent-download URL.
    const regex = /\/web(\/.*)?/; 
    // This will extract the path segment after "web"  
    const path = new URL(request.url).pathname.match(regex)?.[1];
    
    const agentDownloadUrl = new URL('https://api.fpjs.io');
    // Provide extracted path segments as the path of the download URL  
    agentDownloadUrl.pathname = `/web/${path}`  
    
  3. Forward all existing query parameters and add aii monitoring parameter. Use this form for the ii parameter: custom-proxy-integration/<integration_version>/procdn. The ii parameter is necessary to display an integration usage chart in the dashboard. It will help you and the Fingerprint team monitor your integration and diagnose potential issues. Make sure to append, not set the ii parameter to avoid overwriting existing ii parameters.
    agentDownloadUrl.search = request.url.split('?')[1];
    agentDownloadUrl.searchParams.append('ii', `custom-proxy-integration/1.0.1/procdn`);
    
  4. Forward all headers except the cookie header. Because the integration runs in the first-party context it has access to authentication cookies and other sensitive data. Remove all cookies before forwarding the agent download request to Fingerprint.
    const headers = new Headers();
    for (const [key, value] of request.headers.entries()) {
      headers.set(key, value);
    }
    headers.delete('cookie');
    
  5. Request the JavaScript agent from Fingerprint CDN. Use the download URL and headers you have prepared.
    const agentResponse = await fetch(agentDownloadUrl, {
      headers,
    });
    
  6. Set your own cache-control header if necessary. The integration should generally forward the cache-control header it gets from Fingerprint CDN to the client. However, these are GET requests that can end in .js and your cloud infrastructure setup or pre-existing cache mechanism might get in the way and overwrite the cache limits to excessively large values. This would result in the JavaScript agent getting out of date, leading to lower identification accuracy. You might need to experimentally verify if this is happening. If you are not sure, it’s safest to overwrite the cache-control header of the proxied response to ensure the JavaScript agent is not being cached for longer than one minute.
    // If you cannot properly forward the cache-control header, add one manually with low max-age values
    const updatedHeaders = new Headers(agentResponse.headers);
    updatedHeaders.set('cache-control', 'public, max-age=3600, s-maxage=60');
    
  7. Delete encoding headers if necessary. If your HTTP library decompresses or decodes the response automatically (as fetch does in this example), you need to remove these headers to tell the client that the response is not compressed. Alternatively, depending on your HTTP library, you can disable the automatic decompression and keep the headers.
    updatedHeaders.delete('content-encoding');
    updatedHeaders.delete('transfer-encoding');
    
  8. Return the response to the client. No error handling is needed, the proxy integration must return the response as received.
    return new Response(agentResponse.body, {
      status: agentResponse.status,
      statusText: agentResponse.statusText,
      headers: updatedHeaders,
    });
    
  9. Handle internal errors. When the proxy integration itself throws an error, it should return HTTP 500. The response body is optional but recommended.
    export async function GET(request: Request) {
      try {
        // Agent download proxy implementation from above
      } catch (error) {
        return new Response(`Agent download error: ${error} `, {
          status: 500,
        });
      }
    }
    

2. Proxy the Identification request

  1. Create a POST server endpoint. In API v4, the identification path is base path, so it should be available on /RANDOM_PATH that is equal to the path used in the agent download request.
    // Request handler for the identification request
    // Available on POST yourwebsite.com/RANDOM_PATH
    
    export async function POST(request: Request) {
      // Identification request  proxy implementation
    }
    
  2. Use the right identification URLhttps://api.fpjs.io, https://eu.api.fpjs.io, or https://ap.api.fpjs.io depending on your workspace region.
    const identificationUrl = new URL(`https://api.fpjs.io`);
    
  3. Forward all existing query parameters and add an ii monitoring parameter. Use this form for the ii parameter custom-proxy-integration/<integration_version>/ingress. The ii parameter is necessary to display an integration usage chart in the dashboard. It will help you and the Fingerprint team monitor your integration and diagnose potential issues. Make sure to append, not set the ii parameter to avoid overwriting existing ii parameters.
    identificationUrl.search = request.url.split('?')[1] ?? '';
    identificationUrl.searchParams.append('ii', `custom-proxy-integration/1.0/ingress`);
    
  4. Forward all headers as they are, except thecookie header.
    const headers = new Headers();
    for (const [key, value] of request.headers.entries()) {
      headers.set(key, value);
    }
    headers.delete('cookie');
    
  5. Delete all cookies except_iidt. Remove all cookies from the cookie header and include only the _iidt identification cookie if present. The _iidt cookie is set by the Fingerprint server and is needed to ensure high identification accuracy.
    const cookieMap = parseCookies(request.headers.get('cookie'));
    const _iidtCookie = cookieMap['_iidt'];
    if (_iidtCookie) {
      headers.set('cookie', `_iidt=${_iidtCookie}`);
    }
    
  6. Add the necessary Fingerprint headers. Authenticate your request and forward the incoming request’s IP and host to the Fingerprint Identification API. Missing or invalid values of any of these headers will result in failure of the identification request.
    headers.set('FPJS-Proxy-Secret', PROXY_SECRET);
    headers.set('FPJS-Proxy-Client-IP', parseIp(request));
    headers.set('FPJS-Proxy-Forwarded-Host', parseHost(request));
    
    Header KeyHeader Value
    FPJS-Proxy-SecretYour Fingerprint workspace’s proxy secret. You can create one in the Fingerprint dashboard. You can choose to scope the proxy secret to a specific environment.
    FPJS-Proxy-Client-IPThe client IP address of the incoming request. Only accepts valid, public (not private or bogon) IPv4 or IPv6 addresses.
    FPJS-Proxy-Forwarded-HostThe URL host of the incoming request. Only accepts a valid Host string.
    Securely storing the proxy secretThe FPJS-Proxy-Secret header contains a sensitive proxy secret value. To ensure security, do not hard-code this value in the source code as plaintext . Instead, use encrypted environment variables or another secret storage mechanism available on your chosen deployment platform.
    Passing FPJS-Proxy-* header validationThe FPJS-Proxy-* headers sent by your integration are validated by the Fingerprint Identification API.
    • If the FPJS-Proxy-Secret is missing or does not contain a valid proxy secret associated with your Fingerprint workspace and environment, the identification request will fail with a 403 Forbidden status code.
    • If any of the other FPJS-Proxy-* headers is missing, duplicate, or contains an invalid value, the identification request will fail with a 422 Unprocessable Entity status code. For example:
      • FPJS-Proxy-Forwarded-Host is empty or it’s value is not a valid host string.
      • FPJS-Proxy-Client-IP is empty, contains multiple IP addresses, or contains a private or bogon IP address.
    The requirement to use an external IP in the FPJS-Proxy-Client-IP can cause issues when developing or testing your proxy integration on your machine. Even a correct implementation will end up passing a private IP address when running on a local network. To work around this, you can:
    • Temporarily hard-code the FPJS-Proxy-Client-IP header to your public IP address during development.
    • Deploy your integration to a staging environment on a public network for testing.
    • Use a tunneling library like ngrok or LocalTunnel to make your locally running integration available on a public URL and test it from there. Note that not all tunneling libraries correctly forward the true client’s IP address, but we tested both Ngrok and LocalTunnel and found them to work well.
  7. Make the identification request. Use the identification URL and headers you have prepared above. Forward the incoming request’s body without change.
    const identificationResponse = await fetch(identificationUrl, {
      headers: headers,
      method: 'POST',
      body: await request.blob(),
    });
    
  8. Remove the HSTS header if necessary. If your app needs to work using HTTP, remove the strict-transport-security header. Otherwise, forward all headers as they come.
    const updatedHeaders = new Headers(identificationResponse.headers);
    updatedHeaders.delete('strict-transport-security');
    
  9. Return the response to the client. No error handling is needed, the proxy integration must return the response as received.
    return new Response(await identificationResponse.blob(), {
      status: identificationResponse.status,
      statusText: identificationResponse.statusText,
      headers: updatedHeaders,
    });
    
  10. Handle internal errors. When the proxy integration itself throws an error, it should return HTTP 500 using a specific error format shown in the example implementation below.
    • error.message is the message provided by your integration.
    • requestId is a unique ID in the following format:<timestamp>.<id>. The id is a 6-character long string randomly picked from[a-zA-Z0-9]\.
    export async function POST(request: NextRequest) {
      try {
        // Proxy logic defined above
        return await proxyIdentificationRequest(request);
      } catch (error) {
        console.error(error);
        return getErrorResponse(request, error);
      }
    }
    
    const getErrorResponse = (request: Request, error: unknown): Response => {
      const message = isNativeError(error) ? error.message : error;
      const requestId = `${new Date().getTime()}.${randomShortString({ length: 6 })}`;
      console.error(message, requestId);
      return Response.json(
        {
          v: '2',
          error: {
            code: 'IntegrationFailed',
            // You can return a generic error here instead
            // if you don't want to leak information to the client
            message: `An identification error occurred with the custom integration. Reason: ${message}`,
          },
          requestId,
          products: {},
        },
        {
          status: 500,
          headers: {
            'Content-Type': 'application/json',
            'Access-Control-Allow-Origin':
              request.headers.get('origin') ?? new URL(request.url).origin,
            'Access-Control-Allow-Credentials': 'true',
          },
        }
      );
    };
    

3. Proxy the Browser cache request (browsers only)

  1. Create a GET server endpoint. Make it available on the same path as the identification endpoint but expect additional random path segments. For example, if your identification endpoint is available on https://yourwebsite.com/RANDOM_PATH then the JavaScript agent will make GET browser cache requests to URLs like https://yourwebsite.com/RANDOM_PATH/abc345/xyz123.
    // Request handler for the browser cache request
    // Available on yourwebsite.com/RANDOM_PATH/[...randomPathSegments]
    //  e.g. https://yourwebsite.com/RANDOM_PATH/abc345/xyz123
    
    export async function GET(
      request: Request,
      { params }: { params: { randomPathSegments: string[] } }
    ) {
      // Browser cache request proxy implementation
    }
    
  2. Use the random path segments with the identification URL to get the right browser cache URL. For example, if the incoming request is https://yourwebsite.com/RANDOM_PATH/abc345/xyz123 and your identificationUrl was https://api.fpjs.io/ (global region) then the request to Fingerprint servers must have this URL: https://api.fpjs.io/abc345/xyz123.
    const randomPath = params.randomPathSegments.join('/');
    const browserCacheUrl = new URL(`https://api.fpjs.io/${randomPath}`);
    
  3. Forward all query parameters. You don’t need to add any parameters here.
    browserCacheUrl.search = request.url.split('?')[1];
    
  4. Forward all headers except thecookie header.
    const headers = new Headers();
    for (const [key, value] of request.headers.entries()) {
      headers.set(key, value);
    }
    headers.delete('cookie');
    
  5. Make the browser cache request. Use the browser cache URL and headers you have prepared above.
    const browserCacheResponse = await fetch(browserCacheUrl, {
      headers,
    });
    
  6. Return the fresh response to the client. No changes to the provided headers or error handling are necessary here. Make sure that the response is never cached. The response is unique for each browser, any caching could lead to false positives (returning the same visitor ID for different browsers).
    return browserCacheResponse;
    
  7. Handle internal errors. When the proxy integration itself throws an error, it should return HTTP 500. The response body is optional but recommended.
    export async function GET(
      request: Request,
      { params }: { params: { randomPathSegments: string[] } }
    ) {
      try {
        // Browser cache request proxy implementation from above
      } catch (error) {
        return new Response(`Browser cache request error: ${error} `, {
          status: 500,
        });
      }
    }
    
Return 404 for unmatched pathsIf the request path does not match any of the defined paths, the integration must return a HTTP 404. This can happen if there is a typo or a configuration problem with the Fingerprint client agent or the proxy integration itself.

4. Configure the Fingerprint client agent

Configure the Fingerprint client agent to make requests to your integration instead of the default Fingerprint APIs.
  • Set endpoints to the path of your identification proxy endpoint, for example, yourwebsite.com/RANDOM_PATH.
import * as Fingerprint from '@fingerprint/agent'

// Initialize the agent at application startup.
const fp = Fingerprint.start({
  apiKey: <<browserToken>>,
  endpoints: 'https://yourwebsite.com/RANDOM_PATH',
});
If everything is configured correctly, you should receive the latest Fingerprint client-side script and the identification result through your custom proxy integration.

Fallback endpoints

If requests to your proxy integration fail, the Fingerprint client agent will fall back to the default endpoints and keep identifying visitors, albeit without the proxy integration accuracy benefits.

5. Test and monitor your proxy integration

A misconfigured proxy integration or client agent can completely disrupt visitor identification on your website. We recommend testing your integration thoroughly before deploying it to production. If you implemented the ii monitoring parameter correctly, you can go to Dashboard > App setting > Integrations > Custom proxy integration to see the status of your integration. Here you can monitor:
  • The latest used integration version.
  • How many identification requests are coming through the integration (and how many are not).
The information on the status page is cached so allow a few minutes for the latest data points to be reflected. If you have any questions, reach out to our support team.

Migrating from v3 to v4

If you already have a custom proxy integration built for API v3, here is an overview of the changes required to migrate to v4. For more details, see the full guidelines above. We recommend deploying a separate v4 proxy implementation on a new path and then switching to JavaScript agent v4 using the new proxy. Alternatively, you can consider adjusting your existing proxy to support v3 and v4 at the same time, but that is outside the scope of this guide.

Server-side changes

  1. Agent download endpoint — change from query parameter-based to path-based URL construction. In v4, the path is extracted from the request URL instead of using query parameters:
Agent download URL construction
// v3: Query parameter-based URL construction
const queryParams = new URLSearchParams(request.url.split('?')[1]); 
const apiKey = queryParams.get('apiKey'); 
const version = queryParams.get('version') ?? 3; 
const loaderVersion = queryParams.get('loaderVersion'); 
const loaderParam = loaderVersion ? `/loader_v${loaderVersion}.js` : ''; 
const agentDownloadUrl = new URL(`https://fpcdn.io/v${version}/${apiKey}${loaderParam}`); 

// v4: Path-based URL construction
const regex = /\/web(\/.*)?/; 
const path = new URL(request.url).pathname.match(regex)?.[1]; 
const agentDownloadUrl = new URL('https://api.fpjs.io'); 
agentDownloadUrl.pathname = `/web/${path}`; 
  1. Unified base path — in v4, the agent download and identification endpoints share a common base path instead of separate paths:
Endpointv3v4
Agent download/metrics/YOUR_AGENT_PATH/RANDOM_PATH/web/*
Identification/metrics/YOUR_IDENTIFICATION_PATH/RANDOM_PATH
Browser cache/metrics/YOUR_IDENTIFICATION_PATH/[...segments]/RANDOM_PATH/[...segments]

Client-side changes

JavaScript agent (NPM) — update your import and configuration. The v4 SDK uses start() instead of load() and a single endpoints option instead of separate scriptUrlPattern and endpoint options:
Client configuration
import * as FingerprintJS from '@fingerprintjs/fingerprintjs-pro'; 
import * as Fingerprint from '@fingerprint/agent'; 

const fpPromise = FingerprintJS.load({ 
const fp = Fingerprint.start({ 
  apiKey: 'PUBLIC_API_KEY',
  scriptUrlPattern: [ 
    'https://yourwebsite.com/metrics/YOUR_AGENT_PATH?apiKey=<apiKey>&version=<version>&loaderVersion=<loaderVersion>', 
    FingerprintJS.defaultScriptUrlPattern, 
  ], 
  endpoint: [ 
    'https://yourwebsite.com/metrics/YOUR_IDENTIFICATION_PATH', 
    FingerprintJS.defaultEndpoint, 
  ], 
  endpoints: 'https://yourwebsite.com/RANDOM_PATH', 
});
CDN installation — update the script URL format and configuration:
CDN installation
const url = 'https://yourwebsite.com/metrics/YOUR_AGENT_PATH?apiKey=PUBLIC_API_KEY'; 
const url = 'https://yourwebsite.com/RANDOM_PATH/web/v4/PUBLIC_API_KEY'; 
const fpPromise = import(url).then((FingerprintJS) =>
  FingerprintJS.load({ 
const fpPromise = import(url).then((Fingerprint) =>
  Fingerprint.start({ 
    endpoint: [ 
      'https://yourwebsite.com/metrics/YOUR_IDENTIFICATION_PATH', 
      FingerprintJS.defaultEndpoint, 
    ], 
    endpoints: 'https://yourwebsite.com/RANDOM_PATH', 
  })
);