Skip to main content
Glama
findmine

FindMine Shopping Stylist

Official
by findmine
findmine-client.ts10.9 kB
/** * FindMine API Client * Handles all interactions with the FindMine API */ import { FindMineBaseRequest, CompleteTheLookRequest, CompleteTheLookResponse, VisuallySimilarRequest, VisuallySimilarResponse, ItemDetailsUpdateRequest, ItemDetailsUpdateResponse, AnalyticsRequest, AnalyticsResponse, FindMineProduct, } from '../types/findmine-api.js'; /** * Raw look data from API before normalization */ interface RawLook { look_id?: string; id?: string; products?: FindMineProduct[]; items?: FindMineProduct[] | Record<string, FindMineProduct>; [key: string]: unknown; } /** * Raw response from complete-the-look API */ interface RawCompleteTheLookResponse { looks?: RawLook[]; pdp_item?: FindMineProduct; [key: string]: unknown; } /** * Raw error response from API */ interface RawErrorResponse { error?: { message?: string; }; } /** * Configuration for the FindMine API client */ export interface FindMineClientConfig { apiBaseUrl: string; applicationId: string; apiVersion?: string; defaultRegion?: string; defaultLanguage?: string; retryCount?: number; retryDelayMs?: number; } /** * FindMine API client */ export class FindMineClient { private config: FindMineClientConfig; constructor(config: FindMineClientConfig) { this.config = { ...config, apiVersion: config.apiVersion ?? 'v3', retryCount: config.retryCount ?? 3, retryDelayMs: config.retryDelayMs ?? 1000, }; } /** * Make API request with retries */ private async makeRequest<T>( endpoint: string, method: 'GET' | 'POST', params: object ): Promise<T> { let url = `${this.config.apiBaseUrl}${endpoint}`; let body: string | undefined; // Build URL for GET requests, or request body for POST if (method === 'GET') { const queryParams = new URLSearchParams(); const entries = Object.entries(params) as Array<[string, unknown]>; for (const [key, value] of entries) { if (value !== undefined && value !== null) { // Convert primitives safely; skip objects/arrays in query params if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { queryParams.append(key, String(value)); } } } url = `${url}?${queryParams.toString()}`; } else { body = JSON.stringify(params); } // Print request info to stderr for debugging if (process.env.FINDMINE_DEBUG === 'true') { console.error(`[FindMineClient] ${method} ${url}`); console.error(`[FindMineClient] Params: ${JSON.stringify(params)}`); } // Try the request with retries const retryCount = this.config.retryCount ?? 3; let lastError: Error | null = null; for (let attempt = 0; attempt <= retryCount; attempt++) { try { if (process.env.FINDMINE_DEBUG === 'true') { console.error( `[FindMineClient] Request attempt ${String(attempt + 1)} of ${String(retryCount + 1)}` ); } const response = await fetch(url, { method, headers: { 'Content-Type': 'application/json', Accept: 'application/json', }, body: method === 'POST' ? body : undefined, }); // Get response as text first to help with debugging const responseText = await response.text(); // Print response info to stderr for debugging if (process.env.FINDMINE_DEBUG === 'true') { console.error( `[FindMineClient] Response status: ${String(response.status)} ${response.statusText}` ); console.error( `[FindMineClient] Response body: ${responseText.substring(0, 500)}${responseText.length > 500 ? '...' : ''}` ); } // Parse as JSON (if valid) let data: unknown; try { data = JSON.parse(responseText) as unknown; } catch { throw new Error( `Failed to parse API response as JSON: ${responseText.substring(0, 100)}...` ); } if (!response.ok) { const errorData = data as RawErrorResponse; const errorMessage = errorData.error?.message ?? 'Unknown error'; throw new Error(`FindMine API error: ${errorMessage}`); } // Verify and normalize the response structure for complete-the-look if (endpoint.includes('complete-the-look')) { const ctlData = data as RawCompleteTheLookResponse; // Ensure the response has a looks array if (!ctlData.looks || !Array.isArray(ctlData.looks)) { console.error( '[FindMineClient] Warning: API returned response without looks array:', ctlData ); ctlData.looks = []; } // Process each look to normalize structure ctlData.looks = ctlData.looks.map((look: RawLook): RawLook => { const normalizedLook: RawLook = { ...look }; // Ensure a valid look ID if (!normalizedLook.look_id && normalizedLook.id) { normalizedLook.look_id = normalizedLook.id; } // Check if we have items but no products if (!normalizedLook.products && normalizedLook.items) { // Try to extract product data if (Array.isArray(normalizedLook.items)) { // Some API versions return items as an array of products normalizedLook.products = normalizedLook.items; } else if (typeof normalizedLook.items === 'object') { // Some API versions return items as a mapping of id -> product normalizedLook.products = Object.values(normalizedLook.items); } } return normalizedLook; }); return ctlData as T; } return data as T; } catch (error) { lastError = error as Error; if (process.env.FINDMINE_DEBUG === 'true') { console.error( `[FindMineClient] Request failed: ${error instanceof Error ? error.message : String(error)}` ); } // Don't wait on the last attempt if (attempt < retryCount) { const delayMs = this.config.retryDelayMs ?? 1000; await new Promise((resolve) => setTimeout(resolve, delayMs)); } } } throw lastError || new Error('Unknown error in FindMine API'); } /** * Create a base request with common parameters */ private createBaseRequest(sessionId: string, customerId?: string): FindMineBaseRequest { return { application: this.config.applicationId, customer_session_id: sessionId, customer_id: customerId, region: this.config.defaultRegion, language: this.config.defaultLanguage, }; } /** * Get complete the look recommendations for a product */ async getCompleteTheLook( productId: string, inStock: boolean, onSale: boolean, sessionId: string, options: { colorId?: string; customerId?: string; returnPdpItem?: boolean; gender?: 'M' | 'W' | 'U'; apiVersion?: string; } = {} ): Promise<CompleteTheLookResponse> { const apiVersion = options.apiVersion ?? this.config.apiVersion ?? 'v3'; // Create request parameters, only including gender if explicitly provided const params: CompleteTheLookRequest = { ...this.createBaseRequest(sessionId, options.customerId), product_id: productId, product_color_id: options.colorId, product_in_stock: inStock, product_on_sale: onSale, return_pdp_item: options.returnPdpItem, }; // Only add gender param if explicitly provided if (options.gender !== undefined) { params.customer_gender = options.gender; } return this.makeRequest<CompleteTheLookResponse>( `/api/${apiVersion}/complete-the-look`, 'GET', params ); } /** * Get visually similar products */ async getVisuallySimilar( productId: string, sessionId: string, options: { colorId?: string; customerId?: string; limit?: number; offset?: number; gender?: 'M' | 'W' | 'U'; apiVersion?: string; } = {} ): Promise<VisuallySimilarResponse> { const apiVersion = options.apiVersion ?? this.config.apiVersion ?? 'v3'; // Create request parameters, only including gender if explicitly provided const params: VisuallySimilarRequest = { ...this.createBaseRequest(sessionId, options.customerId), product_id: productId, product_color_id: options.colorId, limit: options.limit, offset: options.offset, }; // Only add gender param if explicitly provided if (options.gender !== undefined) { params.customer_gender = options.gender; } return this.makeRequest<VisuallySimilarResponse>( `/api/${apiVersion}/visually-similar`, 'GET', params ); } /** * Update item details (stock status, sale status) */ async updateItemDetails( items: Array<{ productId: string; colorId?: string; inStock: boolean; onSale: boolean; }>, sessionId: string, options: { customerId?: string; apiVersion?: string; } = {} ): Promise<ItemDetailsUpdateResponse> { const apiVersion = options.apiVersion ?? this.config.apiVersion ?? 'v3'; const params: ItemDetailsUpdateRequest = { ...this.createBaseRequest(sessionId, options.customerId), items: items.map((item) => ({ product_id: item.productId, product_color_id: item.colorId, in_stock: item.inStock, on_sale: item.onSale, })), }; return this.makeRequest<ItemDetailsUpdateResponse>( `/api/${apiVersion}/item-details`, 'POST', params ); } /** * Track an analytics event */ async trackEvent( eventType: 'view' | 'click' | 'add_to_cart' | 'purchase', productId: string, sessionId: string, options: { colorId?: string; lookId?: string; sourceProductId?: string; price?: number; quantity?: number; customerId?: string; apiVersion?: string; } = {} ): Promise<AnalyticsResponse> { const apiVersion = options.apiVersion ?? this.config.apiVersion ?? 'v3'; const params: AnalyticsRequest = { ...this.createBaseRequest(sessionId, options.customerId), event_type: eventType, product_id: productId, product_color_id: options.colorId, look_id: options.lookId, source_product_id: options.sourceProductId, price: options.price, quantity: options.quantity, }; return this.makeRequest<AnalyticsResponse>(`/api/${apiVersion}/analytics`, 'POST', params); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/findmine/findmine-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server