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.

DeprecatedThese guidelines are scoped for the JavaScript agent v3, which has been deprecated. For new implementations, use the v4 custom proxy integration guidelines. If you are migrating an existing v3 integration, see the migration guide.
  • 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.

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 likely consist of several server-side API endpoints that proxy requests from Fingerprint client libraries to the Fingerprint servers. Your proxy logic must keep 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 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 randomized path in your application to avoid ad blockers.
    // Request handler for the agent-download request
    // Available on yourwebsite.com/metrics/YOUR_AGENT_PATH, e.g. https://yourwebsite.com/metrics/463n7-d0wnl04d
    
    export async function GET(request: Request) {
      // Agent download proxy implementation
    }
    
  2. Use the query parameters to compile the correct agent-download URL. The query parameters will vary depending on your installation method. Using the NPM package or one of our client libraries requires sending loaderVersion in addition to apiKey and version.
    const queryParams = new URLSearchParams(request.url.split('?')[1]);
    const apiKey = queryParams.get('apiKey');
    const DEFAULT_VERSION = 3;
    const version = queryParams.get('version') ?? DEFAULT_VERSION;
    const loaderVersion = queryParams.get('loaderVersion');
    
    const loaderParam = loaderVersion ? `/loader_v${loaderVersion}.js` : '';
    const agentDownloadUrl = new URL(`https://fpcdn.io/v${version}/${apiKey}${loaderParam}`);
    
  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. Make it available on a randomized path in your application to avoid ad blockers.
    // Request handler for the identification request
    // Available on yourwebsite.com/metrics/YOUR_IDENTIFICATION_PATH, e.g. https://yourwebsite.com/metrics/1d3n71f1c4710n-r35ul7
    
    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 secret

    The 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 validation

    The 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/metrics/YOUR_IDENTIFICATION_PATH then the JavaScript agent will make GET browser cache requests to URLs like https://yourwebsite.com/metrics/YOUR_IDENTIFICATION_PATH/abc345/xyz123.
    // Request handler for the browser cache request
    // Available on yourwebsite.com/metrics/YOUR_IDENTIFICATION_PATH/[...randomPathSegments]
    //  e.g. https://yourwebsite.com/metrics/1d3n71f1c4710n-r35ul7/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/metrics/YOUR_IDENTIFICATION_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 paths

If 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 endpoint to the path of your identification proxy endpoint, for example, yourwebsite.com/metrics/YOUR_IDENTIFICATION_PATH.
  • For browsers and web-based mobile apps, set the scriptUrlPattern to the path of your agent-download proxy endpoint, for example, yourwebsite.com/metrics/YOUR_AGENT_PATH?apiKey=<apiKey>&version=<version>&loaderVersion=<loaderVersion>
    • Keep the query parameters as displayed here, including <and >.
    • The CDN installation method pattern looks slightly different: https://yourwebsite.com/metrics/YOUR_AGENT_PATH?apiKey=PUBLIC_API_KEY, see the full code examples below.
import * as FingerprintJS from '@fingerprintjs/fingerprintjs-pro'

// Initialize the agent at application startup.
const fpPromise = FingerprintJS.load({
  apiKey: <<browserToken>>,
  scriptUrlPattern: [
    'https://yourwebsite.com/metrics/YOUR_AGENT_PATH?apiKey=<apiKey>&version=<version>&loaderVersion=<loaderVersion>',
    FingerprintJS.defaultScriptUrlPattern, // Fallback to default CDN in case of error
  ],
  endpoint: [
    'https://yourwebsite.com/metrics/YOUR_IDENTIFICATION_PATH',
    FingerprintJS.defaultEndpoint // Fallback to default endpoint in case of error
  ],
});
If everything is configured correctly, you should receive the latest Fingerprint client-side script and the identification result through your custom proxy integration.

Always use fallback endpoints

Note the use of FingerprintJS.defaultEndpoint and FingerprintJS.defaultScriptUrlPattern in the examples above. Always provide the default Fingerprint endpoints as backups in addition to your custom proxy 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.

Tip: Using open client response in your proxy integration

To optimize your Fingerprint integration, you can opt into receiving the deobfuscated JavaScript agent response in your proxy integration. This allows you to process the Fingerprint results directly in your proxy infrastructure instead of sending them to your client and then back to the server. This improves both latency and security.