SDKs

Java

Read identification events with a secret key, verify signed webhooks, and serve the browser SDK from your own domain on Java 11 and newer, Kotlin and Scala.

io.fingerly:fingerly-server reads stored events by request ID, verifies webhook signatures, and forwards the browser SDK's requests through a route on your own domain. Its only runtime dependency is jackson-databind.

Requirements

  • Java 11 or newer. Kotlin and Scala use the same artifacts.
  • A secret key for reading events, a webhook signing secret, and a proxy key for the proxy.

Install

<dependency>
  <groupId>io.fingerly</groupId>
  <artifactId>fingerly-server</artifactId>
  <version>0.1.0</version>
</dependency>

Read an event

Create one client with your secret key and reuse it. The key names the environment the client reads.

Java
import io.fingerly.server.FingerlyClient;

FingerlyClient fingerly = FingerlyClient.builder()
    .secretKey(System.getenv("FINGERLY_SECRET_KEY"))
    .build();

Event event = fingerly.events().get("01a0a84b-e6a2-7c09-9f51-0b3d7a26c8e4");

An event has the fields listed in Get an event. suspect_score is null when the request was not scored.

Verify a checkout

Read the event your client identified, check it belongs to this action and is recent, then decide on its level. See server-side verification.

Java
public String decide(String orderId, String requestId) {
    Event event;
    try {
        event = fingerly.events().get(requestId);
    } catch (FingerlyApiException e) {
        if (e.getStatus() == 404) return "refuse";
        throw e;
    }

    if (!("checkout:" + orderId).equals(event.getTag())) return "refuse";
    if (event.getOccurredAt().isBefore(Instant.now().minus(Duration.ofMinutes(2)))) return "refuse";

    return switch (String.valueOf(event.getSuspectLevel())) {
        case "high" -> "review";
        case "medium" -> "challenge";
        default -> "allow";
    };
}

Verify a webhook

Check the signature over the raw request body before parsing it. The helper rejects timestamps more than five minutes from now.

Java
@PostMapping("/webhooks/fingerly")
public ResponseEntity<Void> receive(
        @RequestBody byte[] body,
        @RequestHeader("x-fingerly-timestamp") String timestamp,
        @RequestHeader("x-fingerly-signature") String signature) {

    if (!Webhooks.verify(webhookSecret, body, timestamp, signature)) {
        return ResponseEntity.badRequest().build();
    }

    WebhookEvent event = Webhooks.parse(body);
    events.enqueue(event.getId(), body);   // deduplicate on the event ID
    return ResponseEntity.noContent().build();
}

Artifacts

Java has no single HTTP server standard, so the proxy ships an adapter per standard. Take the core plus whichever one matches your stack; the servlet APIs and Spring are provided, so nothing collides with what your application server already has.

ArtifactContainsFor
io.fingerly:fingerly-serverclient, webhooks, proxy coreeverything
io.fingerly:fingerly-server-jakartaFingerlyProxyFilterSpring Boot 3, Tomcat 10+, Jetty 11+
io.fingerly:fingerly-server-javaxFingerlyProxyFilterSpring Boot 2, Tomcat 9 and older
io.fingerly:fingerly-server-webfluxFingerlyProxyWebFilterreactive Spring, 5.3 and 6.x

Unscored events

Event.getSuspectScore() returns Optional<Integer>, empty when the request was not scored. Never read an empty score as zero: a visitor about whom nothing fired scores 0, while a submission that was never enriched has no score at all. isScored() states the distinction at the call site, and getRaw() keeps the response body so a field the API adds later needs no SDK upgrade.

Read an event you just identified

An event is readable as soon as identification answers, so this needs nothing special.

Read.java
Event event = fingerly.events().get(requestId);

Earlier versions had a waitFor that polled through the gap between identification and the event becoming queryable. There is no gap, so isEventNotFound() now means a request id that was never issued. The archived document arrives moments after the event itself, so check hasDocument() rather than assuming it is there.

Serve the browser SDK from your domain

Content blockers refuse browser requests to third-party APIs, and a refused identification is a visitor never identified. Mount the proxy at the path the dashboard issued for it, in Integration > Proxy keys, and those requests become first-party. See proxy integrations.

FingerlyProxy holds every decision and speaks to no server API. One of the adapters mounts it.

@Bean
FingerlyProxy fingerlyProxy() {
    return FingerlyProxy.builder()
        .proxyKey(System.getenv("FINGERLY_PROXY_KEY"))     // fly_px_production_…
        .path("/k7x2qm4a")
        .clientIp(request -> request.header("CF-Connecting-IP"))
        .build();
}

@Bean
FilterRegistrationBean<FingerlyProxyFilter> fingerlyProxyFilter(FingerlyProxy proxy) {
    var registration = new FilterRegistrationBean<>(new FingerlyProxyFilter(proxy));
    registration.addUrlPatterns("/k7x2qm4a");
    return registration;
}

A filter passes every other path on to your application untouched. It answers 405 to anything but POST, and 401 when the request carries no public key for the same environment as the proxy key. Every request is forwarded to https://api.fingerly.io/api/v1/ingest, and only content-type, x-request-id and the rate limit headers are relayed back, so nothing on the response names Fingerly. Event reads need a secret key and are unreachable through it. Responses are marked no-store, customer cookies and authorization headers are never forwarded, and the proxy reports itself upstream as java/0.1.0, which the dashboard shows per proxy.

API

FingerlyClient.builder()

  • secretKeyStringrequired
    A secret key. Its prefix names the environment read.
  • endpointString
    Override the API origin.
  • apiVersionStringDefault "v1"
    API route version; the server must support the selected version.
  • timeoutDurationDefault 10s
    Deadline per request, covering the response body.
  • httpClientHttpClient
    A custom client. Configure it with Redirect.NEVER.
MethodReturnsCalls
events().get(requestId)EventGET /events/{request_id}.
events().list(params)EventPageGET /events. params may be null; an unset filter is not sent, so the server defaults it.
Webhooks.verify(secret, payload, timestamp, signature)booleanStatic. Never throws for malformed input; a five-minute replay window by default.
Webhooks.parse(payload)WebhookEventThe delivery envelope. getData() stays a JsonNode.
FingerlyProxy.builder()FingerlyProxyThe proxy core, mounted by one of the adapters. .path(...) is the issued path.

Every exception is unchecked and derives from FingerlyException. A non-2xx response throws FingerlyApiException with getStatus(), and optional getCode(), getDetails() and getRequestId(); the secret key is stripped from all three. A deadline throws FingerlyTimeoutException and an unreachable API throws FingerlyConnectionException.