> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://contentful.com/developers/docs/llms.txt. > For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://contentful.com/_mcp/server. # Shared SDK > Work with events and types common to all Ninetailed implementations. ## Shared SDK: The NinetailedApiClient The Shared SDK exposes Ninetailed data objects, types, and methods applicable across runtimes. The Shared SDK allows you to create an API Client that facilitates constructing and sending [Experience API](/personalization/experience-api) events. It also handles retrying requests and request errors. Elements of the shared SDK are composed to create the [JavaScript SDK](/personalization/the-ninetailed-instance), which is then composed into Ninetailed's React-based SDKs. For more information about the code, see the shared SDK in our [open source repository](https://github.com/ninetailed-inc/experience.js/tree/main/packages/sdks/shared/src/lib). ## Instantiation ```typescript import { NinetailedApiClient, fetchImpl} from "@ninetailed/experience.js-shared" const ninetailedApiClient = new NinetailedApiClient({ clientId, environment, url, fetchImpl, }: NinetailedApiClientOptions) type NinetailedApiClientOptions = { clientId: string; environment?: string; url?: string; fetchImpl?: FetchImpl; } ``` | Parameter | Type Signature | Description | | ------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `clientId` | `clientId: string` | The organization ID/API key of a Contentful Personalization account. | | `environment` | `environment?: string` | The environment key of a Contentful Personalization account. Typically either `main` or `development`, depending on the environment in which you have configured your content source connections. Defaults to `main` if unsupplied. | | `url` | `url?: string` | Specify the base URL of the Experience API. This should usually be left unspecified. Defaults to the production Experience API base URL of `https://experience.ninetailed.co` when unspecified. | | `fetchImpl` | `fetchImpl?: FetchImpl` | A `NinetailedApiClient` can be used in different JavaScript runtimes including the browser, Node.js, and edge workers. However, an implementation of the `fetch()` method available in the browser may not be available within all of those run times. This option allows you to provide a `fetch()` implementation of your own should the runtime not expose one automatically. | ## Profile methods A `NinetailedAPIClient` maps one function for each Experience API endpoint. `upsertProfile` is also provided to conveniently switch between create and update functions. | Method | Type Signature | Description | | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `upsertProfile` | `upsertProfile({ profileId, events }: { profileId?: string; events: Event[];} options?: RequestOptions): Promise<ProfileWithSelectedVariants>` | If a profile ID is not supplied, calls `updateProfile`. Otherwise, calls `createProfile`. | | `createProfile` | `createProfile({ events }: { events: Event[];}, options?: RequestOptions): Promise<ProfileWithSelectedVariants>` | Creates a profile with the specified profile ID. Interfaces with the [create profile endpoint](/personalization/experience-api). | | `updateProfile` | `createProfile({ profileId, events }: { profileId: string, events: Event[];}, options?: RequestOptions): Promise<ProfileWithSelectedVariants>` | Creates a profile at the specified profile ID. Interfaces with the [update profile endpoint](/personalization/experience-api). | | `getProfile` | `getProfile(id: string, options?: Omit<RequestOptions, 'preflight' \| 'plaintext'> ): Promise<ProfileWithSelectedVariants>` | Get the profile with the specified profile ID. Interfaces with the [get profile endpoint](/personalization/experience-api). | | `upsertManyProfiles` | `upsertManyProfiles({events} : {events: Event[], options?: { timeout?: number \| undefined; enabledFeatures?: Feature[] \| undefined; }}): Promise<ProfileWithSelectedVariants[]>` | Supply an array of events to update many profiles at once. Responds with the representation of each upserted profile. Interfaces with the [batch upsert profile endpoint](/personalization/experience-api). | The returned response of each method is the same as that of the [Experience API](/personalization/experience-api): a data structure indicating the complete representation of the profile(s) and the Ninetailed Experiences and variants that the Experience API has selected for the profile(s). ### Request options All of profile methods accept request options in addition to the supplied events. These are used to control request timeout & retry behavior, performance, localization, and location resolution. ```typescript type RequestOptions = { /** * A timeout after which a request will get cancelled */ timeout?: number; /** * Return a profile as though events have been submitted without actually storing the profile state * Useful in ESR or SSR contexts */ preflight?: boolean; /** * Determines the locale to which to localize location.city & location.country properties */ locale?: string; /** * An IP address to override the IP lookup behavior, if using also supplying "ip-enrichment" as an enabled feature * Used in ESR or SSR environments to proxy a client's IP instead of using the server's IP */ ip?: string; /** * Sending requests as plain-text bypasses the need for a CORS preflight request, making for a faster request. */ plainText?: boolean; /** * The maximum amount of retries for a request. * Only 503 errors will be retried. The Ninetailed API is aware of which requests are retryable and send a 503 error. * * @default 1 */ retries?: number; /** * The maximum amount of time in ms to wait between retries. * By default the retry will be sent immediately as the Ninetailed API is serverless and sends a 503 error if it could not recover a request internally. * * @default 0 (no delay) */ minRetryTimeout?: number; /** * Activated features which the API should use for this request. */ enabledFeatures?: Feature[]; }; /** * "location" = Attempt to resolve the location of the user based on the IP of the request and where it ingresses to the Experience API * "ip-enrichment" = Attempt to resolve firmographic data with a connected Albacross API Key. See Setup => Customer Data => Albacross */ type Feature = "location" | "ip-enrichment" ``` ## Event building functions Each of the profile methods above accepts an array of events. Event building helper functions facilitate generating the required payload for `page`, `track`, and `identify` events. These builder methods take care of parsing the supplied `ctx.url` and populating `context` properties on events commonly used by Audience rules, including `context.page` and `context.campaign`. ```typescript function buildPageEvent(data: { messageId: string; // A UUID timestamp: number; // Typically assigned as Date.now() ctx: { url: string; // Supply the whole URL, including the protocol, domain, path, and any query parameters referrer: string; locale: string; userAgent: string; document?: { title: string; } | undefined; } // Optionally supply an object to merge with API-resolved location location?: Geolocation; properties: Record; // JSON }) function buildTrackEvent(data: { messageId: string; // A UUID timestamp: number; // Typically assigned as Date.now() ctx: { url: string; // Supply the whole URL, including the protocol, domain, path, and any query parameters referrer: string; locale: string; userAgent: string; document?: { title: string; } | undefined; } // Optionally supply an object to merge with API-resolved location location?: Geolocation; event: string; // the name of the event properties: Record; // JSON }) function buildIdentifyEvent(data: { messageId: string; // A UUID timestamp: number; // Typically assigned as Date.now() ctx: { url: string; // Supply the whole URL, including the protocol, domain, path, and any query parameters referrer: string; locale: string; userAgent: string; document?: { title: string; } | undefined; } // Optionally supply an object to merge with API-resolved location location?: Geolocation; userId: string; // An alias. If you don't want to set one, use an empty string "" traits: Record; // JSON }) type Geolocation = { coordinates?: { latitude: number; longitude: number; }, city?: string; // Use proper capitalization of the city name postalCode?: string; region?: string; regionCode?: string; // ISO 3166-2 country?: string; countryCode?: string; // ISO 3166-2 continent?: string; timezone?: string; } ``` ## Example shared SDK usage This is a short example of using the `buildPageEvent` helper to create a well-formatted event of type `page` and using it to upsert a profile. ```typescript import {buildPageEvent, buildTrackEvent, buildIdentifyEvent, NinetailedApiClient, NINETAILED_ANONYMOUS_ID_COOKIE } from "@ninetailed/experience.js-shared" import { v4 as uuidv4 } from 'uuid'; const clientID = "YOUR_NINETAILED_CLIENT_ID" || undefined; const apiClient = new NinetailedApiClient({ clientId }); const middleware = (req: Request) => { buildPageEvent({ // Hardcoded strings are for example purposes // You'd populate these from the incoming Request object ctx: { url: 'https://www.example.com/path/?query=test' locale: 'en-US', referrer: 'https://www.google.com/', userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36', } messageId: uuidv4(), timestamp: Date.now(), // Resolve your original request geolocation info and pass it along location: { city: request.geo?.city, region: request.geo?.region, country: request.geo?.country, continent: request.geo?.continent, } properties: {}, }); const apiResponse = await apiClient.upsertProfile( { // Use cookies to store and read a Ninetailed profile ID // `upsertProfile` takes care of case where this isn't present profileId: req.cookies.get(NINETAILED_ANONYMOUS_ID_COOKIE)?.value events: [{ pageEvent }], }, { ip: req.ip } // Pass original IP address ); // If no profile ID cookie present, be sure to write it in a response return apiResponse; } ``` Despite the brevity of this example, this is all it takes to get started [working with Ninetailed in edge functions and middleware](https://github.com/ninetailed-inc/ninetailed-examples/blob/main/marketing-contentful-next-app-all-in/middleware.ts#L31). > Work with events and types common to all Ninetailed implementations.