Skip to content

INTER-1488: Migrate SDK to APIv4#218

Open
erayaydin wants to merge 64 commits intomainfrom
migrate-api-v4-inter-1488
Open

INTER-1488: Migrate SDK to APIv4#218
erayaydin wants to merge 64 commits intomainfrom
migrate-api-v4-inter-1488

Conversation

@erayaydin
Copy link
Member

@erayaydin erayaydin commented Oct 29, 2025

This PR upgrades the Server SDK to the Server API v4. It removes Server APIv3 specific behavior and aligns the SDK with the new events format shared by Server API and Webhooks.

❓ Why?

  • Server API v3 will be deprecated.
  • v4 standardizes responses (no data/products/result nesting), switches to Bearer auth, unifies Webhooks & Server API event formats, and deprecates /visitors in favor of /v4/events.

⚙️ What Changed?

  • Endpoints & paths
    • GET /events/search -> GET /v4/events
    • GET /visitors -> removed, migrate to GET /v4/events
    • Request builder now constructs /v4/* URLs.
  • Authentication
    • Replace Auth-Api-Key custom header and apiKey query param with Authorization: Bearer <secret_api_key>.
    • Remove AuthenticationMode option.
  • Names
    • request_id -> event_id
    • Use snake_case in request and response models (e.g., linked_id, pagination_key)
  • Responses
    • Remove legacy top-level nesting
    • Align types with unified events schema
  • Search and Examples
    • Implement GET /v4/events with filters.
    • Update examples and mocked responses
  • Get Event
    • Operation now accepts an optional second parameter (object) with ruleset_id key to evaluate the event against a ruleset.
  • Types
    • Added EventRuleAction type exported for package consumers.
  • Update Event
    • PUT /events -> PATCH /v4/event/:event_id
    • Body fields now snake_case
  • Docs
    • README updated for v4
  • Package
    • Package name changed to @fingerprint/fingerprint-server-sdk
    • Repository URL will be changed to fingerprintjs/node-sdk

⚠️ Breaking Changes

  • Auth: only Bearer is supported. Query and custom header API key modes and AuthenticationMode option are removed.
  • Endpoints/Signatures:
    • updateEvent(body, eventId) now use snake_case fields for body
  • Models: v3 style top-level nesting removed. Consumers must map to the v4 unified event format.

📦 Migration Guide (SDK Consumers)

  • Remove old package and install the new one: @fingerprint/fingerprint-server-sdk
  • Remove authenticationMode option when initializing FingerprintJsServerApiClient.
const client = new FingerprintJsServerApiClient({
  apiKey: '<SECRET_API_KEY>',
  region: Region.Global,
-  authenticationMode: AuthenticationMode.AuthHeader
})
  • Use snake_case fields when updating an event.
client.updateEvent({
- linkedId: "linkedId"
+ linked_id: "linkedId"
}, "<event-id>")
  • Use tags instead of tag for updating an event.
client.updateEvent({
- tag: {
+ tags: {
    key: 'value',
  }, 
}, "<event-id>")
  • Remove getVisitorHistory() function and use searchEvents function.
  • Remove getVisits() function. Use searchEvents() function.
- client.getVisits('<visitorId>')
+ client.searchEvents({ visitor_id: "<visitorId>" })
  • Remove getRelatedVisitors() function.
  • Remove VisitorHistoryFilter, ErrorPlainResponse, VisitorsResponse, RelatedVisitorsResponse, RelatedVisitorsFilter, Webhook, EventsUpdateRequest type usages.

- Switch base URLs to APIv4 (`https://{region}.api.fpjs.io/v4`) and add
  `apiVersion` handling in URL builder.
- Use `Authorization: Bearer <secret-api-key>` only. Remove custom
  header and query api authorization.
- Event API renamed and updated. Use `event_id` instead of `request_id`.
  Also use `PATCH` for event update instead of `PUT` method.
- Add new examples, mocked responses, and update `sync.sh` script.
- README reworked for v4
- Package renamed to `@fingerprint/fingerprint-server-sdk`. Updated
  description and subpackages (example).
- Regenerated types and OpenAPI schema.
- Updated unit and mock tests for v4 URLs.

BREAKING CHANGES:
- Only **Bearer auth** is supported; query and custom-header API-key
  modes are removed. `AuthenticationMode` option is deleted.
- Event endpoint and signatures changes:
  - Use `client.getEvent(eventId)` instead of `requestId`

Related-Task: INTER-1488
Remove `Accept: application/json` testing header for `updateEvent`
function.

Related-Task: INTER-1488
Fix updateEventTests test expected method to `PATCH`.

Related-Task: INTER-1488
Removed unnecessary commented line.

Related-Task: INTER-1488
Expands `searchEvents` JSDoc with new filters. Also its align parameter
names.
Removes `example/getVisitorHistory.mjs` file because it's not a
different endpoint anymore.

Related-Task: INTER-1488
Add `fingerprint-server-sdk-smoke-tests` to changeset ignore list.

Related-Task: INTER-1488
Explicitly mention about package name change in changeset file.

Related-Task: INTER-1488
Use correct `patch` method instead of `put` for updating an event.

Related-Task: INTER-1488
Replace strict `response.status === 200` checks with `response.ok` in
fetch handlers.

Related-Task: INTER-1488
Added a new `callApi()` function to handle request building and `fetch`
usage in one place. Replaced all direct `fetch` calls with `callApi`.
Introduced `defaultHeaders` options to the client, it's include
`Authorization` header and allow extra headers and override of
`Authorization` header. Made `region` optional in `GetRequestPathOptions`
and it's default to `Region.Global` in `getRequestPath` function.

Related-Task: INTER-1488
Remove `isEmptyValue` helper function and use direct `== null` checks to
skip `undefined` or `null` values.

Related-Task: INTER-1488
Use correct `EVENT_ID` placeholder for the example `.env.example` dotenv
file.

Related-Task: INTER-1488
Add `IsNever` and `NonNeverKeys` utility types to filter out `never`
type keys. Introduce and export `AllowedMethod` that excludes specific
`parameters` and any `never` type methods. Use this `AllowedMethod` type
for `GetRequestPathOptions` type and `getRequestPath` functions. Update
related signatures.

Related-Task: INTER-1488
Use `options.method.toUpperCase()` when calling `fetch` to ensure
standard HTTP method casing and avoid test fails. Change `AllowedMethod`
to make a string literal union.

Related-Task: INTER-1488
Extend `callApi` to accept an optional `body?: BodyInit` and forward it
to `fetch`. Fix `updateEvent` to send `body`.

Related-Task: INTER-1488
Removed unnecessary visitor detail tests.

Related-Task: INTER-1488
Fix missing quote for `linked_id` in `searchEvents.mjs` example file.

Related-Task: INTER-1488
Removed redundant `await` keyword in `callApi` function.

Related-Task: INTER-1488
Centralize response parsing and error handling in `callApi` function.
Throw consistent `SdkError`, `RequestError`, or `TooManyRequestsError`
depends on the case. Added `SuccessJsonOrVoid<Path, Method>` to
automatically resolve the potential/correct return type for success
responses. Simplify all public methods.

Related-Task: INTER-1488
Removed `handleErrorResponse` and moved all error logic into `callApi`.
Exported `isErrorResponse` to detect valid error payloads. Added
`createResponse` helper for creating mock Response object with headers.

Related-Task: INTER-1488
Fixes webhook example and description in the `readme.md` file.

Related-Task: INTER-1488
Use `Event` type instead of duplicated `EventsGetResponse` type. Update
sealedResults snapshot and example base64 sealed result value. Fix
reference links.

Related-Task: INTER-1488
Removes unnecessary `ErrorJson` type from the project.

Related-Task: INTER-1488
@erayaydin erayaydin self-assigned this Oct 29, 2025
Fix inconsistent error message casing in the `deleteVisitorData`
function.

Related-Task: INTER-1488
Copy link
Contributor

@TheUnderScorer TheUnderScorer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work! Left some comments, thanks!


if (response.ok) {
if (!isJson || response.status === 204) {
return undefined as SuccessJsonOrVoid<Path, Method>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m afraid this type of assertion could potentially break the contract in some edge cases.
I suggest creating a separate method for operations that don't return anything, such as callApiVoid.


handleErrorResponse(jsonResponse, response)
if (response.status === 429) {
throw new TooManyRequestsError(errPayload, response)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can get rid of the TooManyRequestsError, in V4 the Retry-After header is no longer being sent, so it doesn't make sense to have a separate error instance for this status code.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the OpenAPI schema, it appears we return a 429 Too Many Requests response. We might have forgotten to remove these from the schema if the server definitely doesn't return a 429

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can it be that it returns 429 Response, just without the Retry-After header? Best to test practically to confirm

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In local testing, I saw that it does throw a 429 error, but without retry-after header. I'm thinking of keeping the TooManyRequestsError but removing the retryAfter handling inside it. Is there any objection to not removing the TooManyRequestsError?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed retryAfter handling with commit #4677ab6

erayaydin and others added 4 commits February 24, 2026 20:34
Move `region` parameter passing to inside of the `callApi`.

Related-Task: INTER-1488
Extract `toError` function of `serverApiClient` to `errors` module.

Related-Task: INTER-1488
Add a second `@example` block showing how to handle the `rule_action`
response when calling `getEvent` with a `rulesetId` parameter.

Related-Task: INTER-1488
Co-authored-by: Przemysław Żydek <przemyslawzydek@gmail.com>
Copy link
Contributor

@JuroUhlar JuroUhlar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this, left some suggestions!


handleErrorResponse(jsonResponse, response)
if (response.status === 429) {
throw new TooManyRequestsError(errPayload, response)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can it be that it returns 429 Response, just without the Retry-After header? Best to test practically to confirm

throw TypeError('VisitorId is not set')
}

private async callApi<Path extends keyof paths, Method extends AllowedMethod<Path>>(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want the challenge the need for a generic callApi method. It seem to require a lot of generic type gymnastics to make it work and it might not be worth it. You only have 4 API endpoints. It might be easier typing out their interfaces individually and extracting common code into helper methods.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My main goal with this approach was to minimize errors during usage (in callApi calls within new function/operation implementations). For example, with the current type definition, when calling callApi, you can only specify acceptable method, pathParams, and queryParams specific to the given path. With usages like below, TS errors are immediately visible.

return this.callApi({
  path: '/events/{event_id}',
  pathParams: [eventId],
  method: 'put', // TS2322: Type "put" is not assignable to type "get" | "patch"
  queryParams: rulesetId ? { ruleset_id: rulesetId } : undefined,
})
return this.callApi({
  path: '/non-exists', // TS2322: Type "/non-exists" is not assignable to type keyof paths
  pathParams: [eventId],
  method: 'get',
  queryParams: rulesetId ? { ruleset_id: rulesetId } : undefined,
})
return this.callApi({
  path: '/events/{event_id}',
  pathParams: [eventId],
  method: 'get',
  queryParams: { nonQueryParam: 'ok' }, // TS2353: Object literal may only specify known properties, and nonQueryParam does not exist in type { ruleset_id?: string | undefined; }
})
return this.callApi({
  path: '/events/{event_id}',
  pathParams: [eventId],
  method: 'get',
  queryParams: rulesetId ? { ruleset_id: 1 } : undefined, // TS2322: Type { ruleset_id: number; } | undefined is not assignable to type
})

But we could achieve a similar benefit manually, as you suggested, rather than doing it automatically like this

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am all for maximum type safety, but the mistakes these types catch are unlikely to happen and would be immediately caught by smoke/e2e tests. Consider how much complexity you can remove by relaxing the callApi types and keeping strong type safety just on the user-level methods. Both approach are fine, just different trade-offs, happy to hear more opinions cc @TheUnderScorer @mcnulty-fp

diff --git a/src/serverApiClient.ts b/src/serverApiClient.ts
index 7ba0514..f954fc7 100644
--- a/src/serverApiClient.ts
+++ b/src/serverApiClient.ts
@@ -1,10 +1,15 @@
-import { AllowedMethod, getRequestPath, GetRequestPathOptions, SuccessJsonOrVoid } from './urlUtils'
+import { getRequestPath, GetRequestPathOptions } from './urlUtils'
 import { Event, EventUpdate, FingerprintApi, Options, Region, SearchEventsFilter, SearchEventsResponse } from './types'
-import { paths } from './generatedApiTypes'
 import { RequestError, SdkError, TooManyRequestsError } from './errors/apiErrors'
 import { isErrorResponse } from './errors/handleErrorResponse'
 import { toError } from './errors/toError'
 
+type CallApiOptions = GetRequestPathOptions & {
+  headers?: Record<string, string>
+  body?: BodyInit
+  expect: 'json' | 'void'
+}
+
 export class FingerprintJsServerApiClient implements FingerprintApi {
   public readonly region: Region
 
@@ -87,6 +92,7 @@ export class FingerprintJsServerApiClient implements FingerprintApi {
       pathParams: [eventId],
       method: 'get',
       queryParams: rulesetId ? { ruleset_id: rulesetId } : undefined,
+      expect: 'json',
     })
   }
 
@@ -142,6 +148,7 @@ export class FingerprintJsServerApiClient implements FingerprintApi {
       pathParams: [eventId],
       method: 'patch',
       body: JSON.stringify(body),
+      expect: 'void',
     })
   }
 
@@ -184,6 +191,7 @@ export class FingerprintJsServerApiClient implements FingerprintApi {
       path: '/visitors/{visitor_id}',
       pathParams: [visitorId],
       method: 'delete',
+      expect: 'void',
     })
   }
 
@@ -250,12 +258,13 @@ export class FingerprintJsServerApiClient implements FingerprintApi {
       path: '/events',
       method: 'get',
       queryParams: filter,
+      expect: 'json',
     })
   }
 
-  private async callApi<Path extends keyof paths, Method extends AllowedMethod<Path>>(
-    options: GetRequestPathOptions<Path, Method> & { headers?: Record<string, string>; body?: BodyInit }
-  ): Promise<SuccessJsonOrVoid<Path, Method>> {
+  private async callApi<T>(options: CallApiOptions & { expect: 'json' }): Promise<T>
+  private async callApi(options: CallApiOptions & { expect: 'void' }): Promise<void>
+  private async callApi<T>(options: CallApiOptions): Promise<T | void> {
     const url = getRequestPath({
       ...options,
       region: this.region,
@@ -272,37 +281,39 @@ export class FingerprintJsServerApiClient implements FingerprintApi {
         body: options.body,
       })
     } catch (e) {
-      throw new SdkError('Network or fetch error', undefined, e as Error)
+      throw new SdkError('Network or fetch error', undefined, toError(e))
     }
 
-    const contentType = response.headers.get('content-type') ?? ''
-    const isJson = contentType.includes('application/json')
-
     if (response.ok) {
-      if (!isJson || response.status === 204) {
-        return undefined as SuccessJsonOrVoid<Path, Method>
-      }
-      let data
-      try {
-        data = await response.clone().json()
-      } catch (e) {
-        throw new SdkError('Failed to parse JSON response', response, toError(e))
+      if (options.expect === 'void') {
+        return
       }
-      return data as SuccessJsonOrVoid<Path, Method>
+      return this.parseJson(response)
+    }
+
+    const errorPayload = await this.parseJson<unknown>(response)
+
+    if (response.status === 429 && isErrorResponse(errorPayload)) {
+      throw new TooManyRequestsError(errorPayload, response)
     }
+    if (isErrorResponse(errorPayload)) {
+      throw new RequestError(
+        errorPayload.error.message,
+        errorPayload,
+        response.status,
+        errorPayload.error.code,
+        response
+      )
+    }
+
+    throw RequestError.unknown(response)
+  }
 
-    let errPayload
+  private async parseJson<T>(response: Response): Promise<T> {
     try {
-      errPayload = await response.clone().json()
+      return (await response.clone().json()) as T
     } catch (e) {
       throw new SdkError('Failed to parse JSON response', response, toError(e))
     }
-    if (response.status === 429) {
-      throw new TooManyRequestsError(errPayload, response)
-    }
-    if (isErrorResponse(errPayload)) {
-      throw new RequestError(errPayload.error.message, errPayload, response.status, errPayload.error.code, response)
-    }
-    throw RequestError.unknown(response)
   }
 }
diff --git a/src/types.ts b/src/types.ts
index c51ef28..3a7ed32 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,4 +1,4 @@
-import { components, operations, paths } from './generatedApiTypes'
+import { components, paths } from './generatedApiTypes'
 
 export enum Region {
   EU = 'EU',
@@ -47,56 +47,9 @@ export type EventUpdate = components['schemas']['EventUpdate']
 
 export type EventRuleAction = components['schemas']['EventRuleAction']
 
-// Extract just the `path` parameters as a tuple of strings
-type ExtractPathParamStrings<Path> = Path extends { parameters: { path: infer P } }
-  ? P extends Record<string, any>
-    ? [P[keyof P]] // We extract the path parameter values as a tuple of strings
-    : []
-  : []
-
-// Utility type to extract query parameters from an operation and differentiate required/optional
-export type ExtractQueryParams<Path> = Path extends { parameters: { query?: infer Q } }
-  ? undefined extends Q // Check if Q can be undefined (meaning it's optional)
-    ? Q | undefined // If so, it's optional
-    : Q // Otherwise, it's required
-  : never // If no query parameters, return never
-
-// Utility type to extract request body from an operation (for POST, PUT, etc.)
-type ExtractRequestBody<Path> = Path extends { requestBody: { content: { 'application/json': infer B } } } ? B : never
-
-// Utility type to extract the response type for 200 status code
-type ExtractResponse<Path> = Path extends { responses: { 200: { content: { 'application/json': infer R } } } }
-  ? R
-  : void
-
-// Utility type to check union
-type IsUnion<T, U = T> = T extends U ? ([U] extends [T] ? false : true) : never
-
-// When query params have a single key, flatten it into an optional arg(`getEvent`) else keep as object(`searchEvents`)
-// WARN: This type only affects the public FingerprintApi interface (`serverApiClient`).
-//   For internal request building (`urlUtils`), use `ExtractQueryParams` which preserves the object form.
-type QueryParamArgs<Q> = [Q] extends [never]
-  ? []
-  : [Exclude<Q, undefined>] extends [never]
-    ? []
-    : IsUnion<keyof Exclude<Q, undefined>> extends false
-      ? [param?: Exclude<Q, undefined>[keyof Exclude<Q, undefined>]]
-      : [params: Q]
-
-// Extracts args to given API method
-type ApiMethodArgs<Path extends keyof operations> = [
-  // If method has body, extract it as first parameter
-  ...(ExtractRequestBody<operations[Path]> extends never ? [] : [body: ExtractRequestBody<operations[Path]>]),
-  // Next are path params, e.g. for path "/events/{event_id}" it will be one string parameter,
-  ...ExtractPathParamStrings<operations[Path]>,
-  // Last parameter will be the query params, if any
-  ...QueryParamArgs<ExtractQueryParams<operations[Path]>>,
-]
-
-type ApiMethod<Path extends keyof operations> = (
-  ...args: ApiMethodArgs<Path>
-) => Promise<ExtractResponse<operations[Path]>>
-
-export type FingerprintApi = {
-  [Operation in keyof operations]: ApiMethod<Operation>
+export interface FingerprintApi {
+  getEvent(eventId: string, rulesetId?: string): Promise<Event>
+  updateEvent(body: EventUpdate, eventId: string): Promise<void>
+  searchEvents(filter: SearchEventsFilter): Promise<SearchEventsResponse>
+  deleteVisitorData(visitorId: string): Promise<void>
 }
diff --git a/src/urlUtils.ts b/src/urlUtils.ts
index 27f53d7..042e6ef 100644
--- a/src/urlUtils.ts
+++ b/src/urlUtils.ts
@@ -1,4 +1,4 @@
-import { ExtractQueryParams, Region } from './types'
+import { Region } from './types'
 import { version } from '../package.json'
 import { paths } from './generatedApiTypes'
 
@@ -10,8 +10,7 @@ const globalRegionUrl = 'https://api.fpjs.io/'
 
 type QueryStringScalar = string | number | boolean | null | undefined
 
-type QueryStringParameters = Record<string, QueryStringScalar | string[]> & {
-  api_key?: string
+type QueryStringParameters = Record<string, QueryStringScalar | QueryStringScalar[]> & {
   ii: string
 }
 
@@ -57,67 +56,15 @@ function getServerApiUrl(region: Region): string {
   }
 }
 
-/**
- * Extracts parameter placeholders into a literal union type.
- * For example `extractPathParams<'/users/{userId}/posts/{postId}'>` resolves to `"userId" | "postId"
- */
-type ExtractPathParams<T extends string> = T extends `${string}{${infer Param}}${infer Rest}`
-  ? Param | ExtractPathParams<Rest>
-  : never
-
-type PathParams<Path extends keyof paths> =
-  ExtractPathParams<Path> extends never
-    ? { pathParams?: never }
-    : {
-        pathParams: ExtractPathParams<Path> extends never ? never : string[]
-      }
-
-type QueryParams<Path extends keyof paths, Method extends keyof paths[Path]> =
-  ExtractQueryParams<paths[Path][Method]> extends never
-    ? { queryParams?: any } // No query params
-    : {
-        queryParams?: ExtractQueryParams<paths[Path][Method]> // Optional query params
-      }
-
-type IsNever<Type> = [Exclude<Type, undefined>] extends [never] ? true : false
-export type NonNeverKeys<Type> = {
-  [Key in keyof Type]-?: IsNever<Type[Key]> extends true ? never : Key
-}[keyof Type]
-export type AllowedMethod<Path extends keyof paths> = Extract<Exclude<NonNeverKeys<paths[Path]>, 'parameters'>, string>
-
-type JsonContentOf<Response> = Response extends { content: { 'application/json': infer T } } ? T : never
-
-type UnionJsonFromResponses<Response> = {
-  [StatusCode in keyof Response]: JsonContentOf<Response[StatusCode]>
-}[keyof Response]
+export type HttpMethod = 'get' | 'put' | 'post' | 'delete' | 'options' | 'head' | 'patch' | 'trace'
 
-type StartingWithSuccessCode<Response> = {
-  [StatusCode in keyof Response]: `${StatusCode & number}` extends `2${number}${number}` ? StatusCode : never
-}[keyof Response]
-
-type SuccessResponses<Response> = Pick<Response, Extract<StartingWithSuccessCode<Response>, keyof Response>>
-
-type OperationOf<Path extends keyof paths, Method extends AllowedMethod<Path>> = paths[Path][Method]
-
-type ResponsesOf<Path extends keyof paths, Method extends AllowedMethod<Path>> =
-  OperationOf<Path, Method> extends { responses: infer Response } ? Response : never
-
-type SuccessJson<Path extends keyof paths, Method extends AllowedMethod<Path>> = UnionJsonFromResponses<
-  SuccessResponses<ResponsesOf<Path, Method>>
->
-
-export type SuccessJsonOrVoid<Path extends keyof paths, Method extends AllowedMethod<Path>> = [
-  SuccessJson<Path, Method>,
-] extends [never]
-  ? void
-  : SuccessJson<Path, Method>
-
-export type GetRequestPathOptions<Path extends keyof paths, Method extends AllowedMethod<Path>> = {
-  path: Path
-  method: Method
+export interface GetRequestPathOptions {
+  path: keyof paths
+  method: HttpMethod
+  pathParams?: string[]
+  queryParams?: Record<string, QueryStringScalar | QueryStringScalar[]>
   region?: Region
-} & PathParams<Path> &
-  QueryParams<Path, Method>
+}
 
 /**
  * Formats a URL for the FingerprintJS server API by replacing placeholders and
@@ -125,18 +72,18 @@ export type GetRequestPathOptions<Path extends keyof paths, Method extends Allow
  *
  * @internal
  *
- * @param {GetRequestPathOptions<Path, Method>} options
- * @param {Path} options.path - The path of the API endpoint
+ * @param {GetRequestPathOptions} options
+ * @param {keyof paths} options.path - The path of the API endpoint
  * @param {string[]} [options.pathParams] - Path parameters to be replaced in the path
- * @param {QueryParams<Path, Method>["queryParams"]} [options.queryParams] - Query string
+ * @param {GetRequestPathOptions["queryParams"]} [options.queryParams] - Query string
  *   parameters to be appended to the URL
  * @param {Region} options.region - The region of the API endpoint
- * @param {Method} options.method - The method of the API endpoint
+ * @param {HttpMethod} options.method - The method of the API endpoint
  *
  * @returns {string} The formatted URL with parameters replaced and query string
  *   parameters appended
  */
-export function getRequestPath<Path extends keyof paths, Method extends AllowedMethod<Path>>({
+export function getRequestPath({
   path,
   pathParams,
   queryParams,
@@ -144,7 +91,7 @@ export function getRequestPath<Path extends keyof paths, Method extends AllowedM
   // method mention here so that it can be referenced in JSDoc
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
   method: _,
-}: GetRequestPathOptions<Path, Method>): string {
+}: GetRequestPathOptions): string {
   // Step 1: Extract the path parameters (placeholders) from the path
   const placeholders = Array.from(path.matchAll(/{(.*?)}/g)).map((match) => match[1])
 

Copy link
Member Author

@erayaydin erayaydin Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But in this form, it doesn't show the operations that need to be implemented/updated. It won't show any errors when a new operation or a new parameter is added. We'll need to remember to update this interface and functions whenever a new parameter or operation is added.

export interface FingerprintApi {
  getEvent(eventId: string, { rulesetId?: string }): Promise<Event>
  updateEvent(body: EventUpdate, eventId: string): Promise<void>
  searchEvents(filter: SearchEventsFilter): Promise<SearchEventsResponse>
  deleteVisitorData(visitorId: string): Promise<void>
}

This will not show an error when a new parameter is introduced in the OpenAPI schema. But the one below will show an error about a missing operation/argument when running lint & test workflow

export type FingerprintApi = {
  [Operation in keyof operations]: ApiMethod<Operation>
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the absence of more input, let's stick with your approach then :)

Fixes `Handling an event with rule_action` example of the `getEvent`
function.

Related-Task: INTER-1488
Replace the standalone `rulesetId` string parameter with a
`GetEventOptions` object to support future query parameters without
breaking changes. Remove the `QueryParamArgs` utility type and its
support types that is no longer needed.

Related-Task: INTER-1488
@erayaydin erayaydin force-pushed the migrate-api-v4-inter-1488 branch from be7fcb9 to 8b30052 Compare February 25, 2026 12:42
erayaydin and others added 8 commits February 25, 2026 15:49
Removes `Endpoints and method signatures changed.` breaking change note
because it no longer true. There are no named parameters in JS and all
functions keep similar signature.

Related-Task: INTER-1488
Remove the `retryAfter` property from the `TooManyRequestsError`.

Related-Task: INTER-1488
Make `curl` silent unless `TRACE` or `ACTIONS_STEP_DEBUG` is set.

Related-Task: INTER-1488
Rename the test helper to better reflect that it creates a JSON
response.

Related-Task: INTER-1488
Co-authored-by: Juraj Uhlar <juro.uhlar@gmail.com>
Updates OpenAPI schema to latest version.

Related-Task: INTER-1488
Validate rule_action response for events matched by a ruleset.

Related-Task: INTER-1488
*
* When an event is created, it is assigned `linkedId` and `tag` submitted through the JS agent parameters. This information might not be available on the client so the Server API allows for updating the attributes after the fact.
*
* **Warning** It's not possible to update events older than 10 days.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Schema says: Warning It’s not possible to update events older than one month. I guess it improved and we need to update the description here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated with commit #167e25c

@@ -200,38 +187,14 @@ export class FingerprintJsServerApiClient implements FingerprintApi {
*/
public async deleteVisitorData(visitorId: string): Promise<void> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JS doc still references retryAfter

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated with commit #167e25c


console.log('All tests passed')
return 0
} catch (error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

retryAfter below no longer exists. Consider adding @ts-check to start of all mjs files to catch these kinds of errors in the editor/during

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated with commit #167e25c.

We can consider adding a tsconfig.json and using JSDoc for type checking in a separate PR instead of // @ts-check. Let's handle the type checking in .mjsfiles in its own PR

Update updateEvent JSDoc to reflect the new one month retention limit.

Remove TooManyRequestsError.retryAfter usage from docs and tests.

Related-Task: INTER-1488
@erayaydin erayaydin requested a review from JuroUhlar February 26, 2026 12:39
Serialize array query parameters as `key=a&key=b`.

Related-Task: INTER-1488
@github-actions
Copy link
Contributor

🚀 Following releases will be created using changesets from this PR:

@fingerprint/fingerprint-server-sdk@7.0.0-test.0

Major Changes

  • Server APIv3 -> Server APIv4 migration

    • Switch all endpoints to /v4/*.
    • Remove authenticationMode option when initializing FingerprintJsServerApiClient.
    • Rename request_id to event_id.
    • Use snake_case fields when updating an event.
    • Use PATCH method when updating an event.
    • Examples, tests, and docs updated.

    BREAKING CHANGES

    • authenticationMode option removed.
    • Removed getVisits() function.
    • Removed getRelatedVisitors() function.
    • Removed VisitorHistoryFilter, ErrorPlainResponse, VisitorsResponse, RelatedVisitorsResponse,
      RelatedVisitorsFilter, Webhook, EventsUpdateRequest types.
    • Use tags instead of tag for updating an event.
    • Response models changed. (ce2854d)
  • change package name to @fingerprint/fingerprint-server-sdk (385b01b)

Minor Changes

  • add rulesetId parameter to the getEvent operation (3ebae43)

import { isErrorResponse } from './errors/handleErrorResponse'
import { toError } from './errors/toError'

export class FingerprintJsServerApiClient implements FingerprintApi {
Copy link
Contributor

@JuroUhlar JuroUhlar Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now is a good chance to remove js and call this

Suggested change
export class FingerprintJsServerApiClient implements FingerprintApi {
export class FingerprintServerApiClient implements FingerprintApi {

Wdyt @ilfa ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 for this 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants