[{"data":1,"prerenderedAt":159},["ShallowReactive",2],{"doc:\u002Fdocs\u002Fserver-side-verification":3},{"page":4,"toc":142,"updated":158},{"path":5,"title":6,"seoTitle":7,"description":8,"blocks":9},"\u002Fdocs\u002Fserver-side-verification","Server-side verification","Server-side Verification of Identifications","Never trust a result the browser reports. Read the stored event by request ID with a secret key, check it belongs to this action, then decide.",[10,13,18,48,51,74,77,79,116,119,121,125,128,134,137,139],{"type":11,"text":12},"p","Anything a browser or an app returns can be edited by whoever controls it: the visitor ID, the score, the verdicts. Verification closes that gap. Your backend reads the stored event from Fingerly with a secret key, confirms it belongs to the action being taken, and only then decides.",{"type":14,"level":15,"text":16,"id":17},"heading",2,"The flow","the-flow",{"type":19,"steps":20},"steps",[21,26,38,43],{"title":22,"blocks":23},"The client identifies",[24],{"type":11,"text":25},"Call `identify({ tag })` when the visitor acts. Send only the `requestId` to your backend, with the action.",{"title":27,"blocks":28},"Your server reads the event",[29,31],{"type":11,"text":30},"Fetch the event by request ID with your secret key. The answer comes from Fingerly, not from the browser.",{"type":32,"samples":33},"code",[34],{"label":35,"lang":36,"code":37},"Request","bash","curl \"https:\u002F\u002Fus.api.fingerly.io\u002Fapi\u002Fv1\u002Fevents\u002F01a0a84b-e6a2-7c09-9f51-0b3d7a26c8e4\" \\\n  -H \"x-api-key: $FINGERLY_SECRET_KEY\"",{"title":39,"blocks":40},"Your server checks it",[41],{"type":11,"text":42},"Run the four checks below.",{"title":44,"blocks":45},"Your server decides",[46],{"type":11,"text":47},"Allow, challenge, review or refuse, and record the request ID with the outcome.",{"type":14,"level":15,"text":49,"id":50},"Four checks","four-checks",{"type":52,"columns":53,"rows":57},"table",[54,55,56],"Check","How","Stops",[58,62,66,70],[59,60,61],"It exists","The read succeeds. A `404` means no such event in this key's environment.","Made-up or cross-environment request IDs.",[63,64,65],"It is this action","`tag` equals what you expect, such as `checkout:8412`.","An identification from a harmless page replayed at checkout.",[67,68,69],"It is recent","`occurred_at` is within the window your flow allows, such as two minutes.","Old request IDs saved and reused later.",[71,72,73],"It passes your policy","Read `suspect_level`, `triggers` and `visitor_id`.","The fraud you integrated Fingerly for.",{"type":14,"level":15,"text":75,"id":76},"In code","in-code",{"type":11,"text":78},"The same four checks, with each server SDK.",{"type":32,"samples":80},[81,85,89,92,96,100,104,108,112],{"label":82,"lang":83,"code":84},"Node.js","ts","import { load, FingerlyAPIError } from '@fingerly\u002Fnode'\n\nconst fingerly = load({ secretKey: process.env.FINGERLY_SECRET_KEY! })\nconst MAX_AGE_MS = 2 * 60 * 1000\n\nexport async function decide(orderId: string, requestId: string) {\n  let event\n  try {\n    event = await fingerly.events.get(requestId)\n  } catch (error) {\n    if (error instanceof FingerlyAPIError && error.status === 404) return 'refuse'\n    throw error\n  }\n\n  if (event.tag !== 'checkout:' + orderId) return 'refuse'\n  if (Date.now() - Date.parse(event.occurred_at) > MAX_AGE_MS) return 'refuse'\n\n  if (event.suspect_level === 'high') return 'review'\n  if (event.suspect_level === 'medium') return 'challenge'\n  return 'allow'\n}",{"label":86,"lang":87,"code":88},"Python","python","from datetime import datetime, timedelta, timezone\nfrom fingerly import Fingerly, FingerlyAPIError\n\nfingerly = Fingerly(secret_key=os.environ[\"FINGERLY_SECRET_KEY\"])\n\ndef decide(order_id: str, request_id: str) -> str:\n    try:\n        event = fingerly.events.get(request_id)\n    except FingerlyAPIError as error:\n        if error.status == 404:\n            return \"refuse\"\n        raise\n\n    if event.tag != f\"checkout:{order_id}\":\n        return \"refuse\"\n    if datetime.now(timezone.utc) - event.occurred_at > timedelta(minutes=2):\n        return \"refuse\"\n\n    if event.suspect_level == \"high\":\n        return \"review\"\n    if event.suspect_level == \"medium\":\n        return \"challenge\"\n    return \"allow\"",{"label":90,"lang":87,"code":91},"Python (async)","from datetime import datetime, timedelta, timezone\nfrom fingerly import AsyncFingerly, FingerlyAPIError\n\nfingerly = AsyncFingerly(secret_key=os.environ[\"FINGERLY_SECRET_KEY\"])\n\nasync def decide(order_id: str, request_id: str) -> str:\n    try:\n        event = await fingerly.events.get(request_id)\n    except FingerlyAPIError as error:\n        if error.status == 404:\n            return \"refuse\"\n        raise\n\n    if event.tag != f\"checkout:{order_id}\":\n        return \"refuse\"\n    if datetime.now(timezone.utc) - event.occurred_at > timedelta(minutes=2):\n        return \"refuse\"\n\n    return {\"high\": \"review\", \"medium\": \"challenge\"}.get(event.suspect_level, \"allow\")",{"label":93,"lang":94,"code":95},"Go","go","func decide(ctx context.Context, orderID, requestID string) (string, error) {\n    event, err := client.Events.Get(ctx, requestID)\n    var apiErr *fingerly.APIError\n    if errors.As(err, &apiErr) && apiErr.Status == http.StatusNotFound {\n        return \"refuse\", nil\n    } else if err != nil {\n        return \"\", err\n    }\n\n    if event.Tag != \"checkout:\"+orderID || time.Since(event.OccurredAt) > 2*time.Minute {\n        return \"refuse\", nil\n    }\n\n    switch event.SuspectLevel {\n    case \"high\":\n        return \"review\", nil\n    case \"medium\":\n        return \"challenge\", nil\n    }\n    return \"allow\", nil\n}",{"label":97,"lang":98,"code":99},"Java","java","public String decide(String orderId, String requestId) {\n    Event event;\n    try {\n        event = fingerly.events().get(requestId);\n    } catch (FingerlyApiException e) {\n        if (e.getStatus() == 404) return \"refuse\";\n        throw e;\n    }\n\n    if (!(\"checkout:\" + orderId).equals(event.getTag())) return \"refuse\";\n    if (event.getOccurredAt().isBefore(Instant.now().minus(Duration.ofMinutes(2)))) return \"refuse\";\n\n    return switch (String.valueOf(event.getSuspectLevel())) {\n        case \"high\" -> \"review\";\n        case \"medium\" -> \"challenge\";\n        default -> \"allow\";\n    };\n}",{"label":101,"lang":102,"code":103},".NET","csharp","public async Task\u003Cstring> DecideAsync(string orderId, string requestId, CancellationToken ct)\n{\n    Event ev;\n    try\n    {\n        ev = await _fingerly.Events.GetAsync(requestId, ct);\n    }\n    catch (FingerlyApiException e) when (e.Status == 404)\n    {\n        return \"refuse\";\n    }\n\n    if (ev.Tag != $\"checkout:{orderId}\") return \"refuse\";\n    if (DateTimeOffset.UtcNow - ev.OccurredAt > TimeSpan.FromMinutes(2)) return \"refuse\";\n\n    return ev.SuspectLevel switch\n    {\n        \"high\" => \"review\",\n        \"medium\" => \"challenge\",\n        _ => \"allow\",\n    };\n}",{"label":105,"lang":106,"code":107},"PHP","php","\u003C?php\n\nuse Fingerly\\ApiException;\n\nfunction decide(string $orderId, string $requestId): string\n{\n    global $fingerly;\n\n    try {\n        $event = $fingerly->events->get($requestId);\n    } catch (ApiException $e) {\n        if ($e->getStatus() === 404) {\n            return 'refuse';\n        }\n        throw $e;\n    }\n\n    if ($event->tag !== \"checkout:{$orderId}\") {\n        return 'refuse';\n    }\n    if ($event->occurredAt \u003C new DateTimeImmutable('-2 minutes')) {\n        return 'refuse';\n    }\n\n    return match ($event->suspectLevel) {\n        'high' => 'review',\n        'medium' => 'challenge',\n        default => 'allow',\n    };\n}",{"label":109,"lang":110,"code":111},"Ruby","ruby","def decide(order_id, request_id)\n  event = fingerly.events.get(request_id)\n\n  return \"refuse\" unless event.tag == \"checkout:#{order_id}\"\n  return \"refuse\" if event.occurred_at \u003C Time.now - 120\n\n  case event.suspect_level\n  when \"high\" then \"review\"\n  when \"medium\" then \"challenge\"\n  else \"allow\"\n  end\nrescue Fingerly::APIError => e\n  raise unless e.status == 404\n  \"refuse\"\nend",{"label":113,"lang":114,"code":115},"Rust","rust","async fn decide(fingerly: &fingerly::Client, order_id: &str, request_id: &str) -> Result\u003CDecision, fingerly::Error> {\n    let event = match fingerly.events().get(request_id).await {\n        Ok(event) => event,\n        Err(fingerly::Error::Api { status: 404, .. }) => return Ok(Decision::Refuse),\n        Err(error) => return Err(error),\n    };\n\n    if event.tag.as_deref() != Some(&format!(\"checkout:{order_id}\")) {\n        return Ok(Decision::Refuse);\n    }\n    if chrono::Utc::now() - event.occurred_at > chrono::Duration::minutes(2) {\n        return Ok(Decision::Refuse);\n    }\n\n    Ok(match event.suspect_level {\n        Some(Level::High) => Decision::Review,\n        Some(Level::Medium) => Decision::Challenge,\n        _ => Decision::Allow,\n    })\n}",{"type":14,"level":15,"text":117,"id":118},"Timing","timing",{"type":11,"text":120},"An event is usually readable within a few seconds of the identification. If your client sends the request ID in the same moment it receives it, retry a `404` a few times over a few seconds before refusing.",{"type":122,"tone":123,"text":124},"callout","tip","If you only need the verdict at the moment of the action, the identify response already contains it. Verification is what makes it trustworthy: read the event when the decision matters.",{"type":14,"level":15,"text":126,"id":127},"Keep secret keys secret","keep-secret-keys-secret",{"type":129,"items":130},"list",[131,132,133],"Secret keys are refused when a request carries an `Origin` header, so they cannot be used from front-end code.","A secret key only reads its own environment. Use a production secret key to verify production identifications.","Store keys in your secret manager, and revoke and replace one immediately if it leaks.",{"type":14,"level":15,"text":135,"id":136},"Without a request ID","without-a-request-id",{"type":11,"text":138},"If identification failed in the client, your server receives no request ID. Treat that as missing evidence rather than as proof of fraud or of innocence: for example, allow low-risk actions and require a second factor for high-risk ones.",{"type":122,"tone":140,"text":141},"warning","Do not accept a score, visitor ID or verdict sent by the client in place of a request ID. Only the stored event is trustworthy.",[143,144,147,149,151,153,154,155,156,157],{"id":17,"text":16,"level":15},{"id":145,"text":22,"level":146},"step-the-client-identifies",3,{"id":148,"text":27,"level":146},"step-your-server-reads-the-event",{"id":150,"text":39,"level":146},"step-your-server-checks-it",{"id":152,"text":44,"level":146},"step-your-server-decides",{"id":50,"text":49,"level":15},{"id":76,"text":75,"level":15},{"id":118,"text":117,"level":15},{"id":127,"text":126,"level":15},{"id":136,"text":135,"level":15},"2026-09-17T08:28:36.000Z",1789667797514]