> ## Documentation Index
> Fetch the complete documentation index at: https://docs.emergedata.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# TypeScript SDK

> Complete TypeScript implementation for Emerge API

Full TypeScript implementation for integrating with Emerge. Copy these utilities into your project or use as reference.

## Installation

No SDK package required. These examples use standard Node.js crypto and fetch APIs.

```bash theme={null}
# For Node.js < 18, install node-fetch
npm install node-fetch
```

## Link API Client

```typescript theme={null}
import crypto from 'crypto';

interface EmergeConfig {
  clientId: string;
  signingSecret: string;
  apiToken: string;
  redirectUri: string;
}

interface LinkParams {
  userId: string;
}

interface CallbackParams {
  status: 'success' | 'reauthorized' | 'failure';
  state: string;
  uid: string;
  errorCode?: string;
}

class EmergeLinkClient {
  private config: EmergeConfig;
  private stateStore: Map<string, { userId: string; createdAt: number }>;

  constructor(config: EmergeConfig) {
    this.config = config;
    this.stateStore = new Map();
  }

  /**
   * Create a signed link URL for the consent flow
   */
  createLinkUrl(params: LinkParams): { url: string; state: string } {
    // Use ISO 8601 timestamp
    const timestamp = new Date().toISOString();
    const state = crypto.randomBytes(16).toString('hex');

    // Store state for verification
    this.stateStore.set(state, {
      userId: params.userId,
      createdAt: Date.now()
    });

    const urlParams: Record<string, string> = {
      client_id: this.config.clientId,
      redirect_uri: this.config.redirectUri,
      state,
      timestamp,
      uid: params.userId
    };

    // Sort and create signature (raw values, NOT URL-encoded)
    const sortedKeys = Object.keys(urlParams).sort();
    const signatureBase = sortedKeys
      .map(key => `${key}=${urlParams[key]}`)
      .join('&');

    const signature = crypto
      .createHmac('sha256', this.config.signingSecret)
      .update(signatureBase)
      .digest('hex');

    // Build final URL (URLSearchParams handles encoding)
    const finalParams = new URLSearchParams(urlParams);
    finalParams.append('signature', signature);

    return {
      url: `https://link.emergedata.ai/link/start?${finalParams.toString()}`,
      state
    };
  }

  /**
   * Verify callback state parameter
   */
  verifyState(state: string): { valid: boolean; userId?: string } {
    const stored = this.stateStore.get(state);

    if (!stored) {
      return { valid: false };
    }

    // Clean up state after verification
    this.stateStore.delete(state);

    // Check expiration (1 hour)
    if (Date.now() - stored.createdAt > 3600000) {
      return { valid: false };
    }

    return { valid: true, userId: stored.userId };
  }

  /**
   * Parse callback parameters
   */
  parseCallback(query: Record<string, string>): CallbackParams {
    return {
      status: query.status as CallbackParams['status'],
      state: query.state,
      uid: query.uid,
      errorCode: query.error_code
    };
  }

  /**
   * Get consent status for a user
   */
  async getConsentStatus(uid: string): Promise<{
    consents: Array<{
      provider: string;
      scopes: string[];
      valid_until: string;
      status: string;
      issued_at: string;
    }>;
  }> {
    const response = await fetch(
      `https://link.emergedata.ai/consent/status/${uid}`,
      {
        headers: {
          'Authorization': `Bearer ${this.config.apiToken}`
        }
      }
    );

    if (!response.ok) {
      throw new Error(`Consent status failed: ${response.status}`);
    }

    return response.json();
  }

  /**
   * Get export status for a user
   */
  async getExportStatus(uid: string): Promise<{
    uid: string;
    sources: Array<{
      provider: string;
      data_ready: boolean;
      data_landed_at?: string | null;
      export_status?: string | null;
      export_completed_at?: string | null;
    }>;
  }> {
    const response = await fetch(
      `https://link.emergedata.ai/export/status/${uid}`,
      {
        headers: {
          'Authorization': `Bearer ${this.config.apiToken}`
        }
      }
    );

    if (!response.ok) {
      throw new Error(`Export status failed: ${response.status}`);
    }

    return response.json();
  }
}
```

## Query API Client

```typescript theme={null}
interface QueryParams {
  uid: string;
  ingestedBegin?: string;
  ingestedEnd?: string;
  cursor?: string;
  limit?: number;
}

interface QueryResult<T> {
  data: T[];
  count: number;
  hasMore: boolean;
  nextCursor?: string;
  appliedIngestedEnd?: string;
}

interface SearchEntry {
  user_id: string;
  event_id: number;
  url: string | null;
  title: string | null;
  query: string | null;
  intent: string | null;
  category: string | null;
  brands: string[];
  timestamp: string;
  ingested_at: string;
}

interface BrowsingEntry {
  user_id: string;
  event_id: number;
  url: string | null;
  title: string | null;
  page_category: string | null;
  category: string | null;
  brands: string[];
  timestamp: string;
  ingested_at: string;
}

interface YoutubeEntry {
  user_id: string;
  event_id: number;
  url: string | null;
  title: string | null;
  category: string | null;
  brands: string[];
  timestamp: string;
  ingested_at: string;
}

interface AdsEntry {
  user_id: string;
  event_id: number;
  source_domain: string | null;
  landing_domain: string | null;
  url: string | null;
  title: string | null;
  category: string | null;
  brands: Array<string | null>;
  ad_network: string | null;
  timestamp: string;
  ingested_at: string;
}

interface ReceiptsEntry {
  user_id: string;
  event_id: number;
  subject: string | null;
  retailer_name: string | null;
  receipt_currency: string | null;
  total_value: number | null;
  category: string | null;
  brands: Array<string | null>;
  items: Array<{
    name: string | null;
    quantity: number | null;
    unit_price: number | null;
    brand: string | null;
    category: string | null;
  }>;
  timestamp: string;
  ingested_at: string;
}

class EmergeQueryClient {
  private apiToken: string;
  private baseUrl = 'https://query.emergedata.ai/v1';

  constructor(apiToken: string) {
    this.apiToken = apiToken;
  }

  private async request<T>(endpoint: string, params: QueryParams): Promise<QueryResult<T>> {
    const url = new URL(`${this.baseUrl}${endpoint}`);
    url.searchParams.set('uid', params.uid);

    if (params.ingestedBegin) {
      url.searchParams.set('ingested_begin', params.ingestedBegin);
    }
    if (params.ingestedEnd) {
      url.searchParams.set('ingested_end', params.ingestedEnd);
    }
    if (params.cursor) {
      url.searchParams.set('cursor', params.cursor);
    }
    if (params.limit) {
      url.searchParams.set('limit', params.limit.toString());
    }

    const response = await fetch(url.toString(), {
      headers: {
        'Authorization': `Bearer ${this.apiToken}`
      }
    });

    if (!response.ok) {
      throw new Error(`Query failed: ${response.status}`);
    }

    const result = await response.json();
    return {
      data: result.data,
      count: result.count,
      hasMore: result.has_more,
      nextCursor: result.next_cursor,
      appliedIngestedEnd: result.applied_ingested_end
    };
  }

  /**
   * Get search history (sync)
   */
  async getSearch(params: QueryParams): Promise<QueryResult<SearchEntry>> {
    return this.request<SearchEntry>('/sync/get_search', params);
  }

  /**
   * Get browsing history (sync)
   */
  async getBrowsing(params: QueryParams): Promise<QueryResult<BrowsingEntry>> {
    return this.request<BrowsingEntry>('/sync/get_browsing', params);
  }

  /**
   * Get YouTube history (sync)
   */
  async getYoutube(params: QueryParams): Promise<QueryResult<YoutubeEntry>> {
    return this.request<YoutubeEntry>('/sync/get_youtube', params);
  }

  /**
   * Get ad interactions (sync)
   */
  async getAds(params: QueryParams): Promise<QueryResult<AdsEntry>> {
    return this.request<AdsEntry>('/sync/get_ads', params);
  }

  /**
   * Get receipts (sync)
   */
  async getReceipts(params: QueryParams): Promise<QueryResult<ReceiptsEntry>> {
    return this.request<ReceiptsEntry>('/sync/get_receipts', params);
  }

  /**
   * Fetch all records with automatic pagination
   */
  async fetchAll<T>(
    fetcher: (params: QueryParams) => Promise<QueryResult<T>>,
    params: Omit<QueryParams, 'cursor'>
  ): Promise<{ data: T[]; appliedIngestedEnd?: string }> {
    const allData: T[] = [];
    let cursor: string | undefined;
    let appliedEnd: string | undefined;

    do {
      const result = await fetcher({ ...params, cursor });
      allData.push(...result.data);
      appliedEnd = result.appliedIngestedEnd;
      cursor = result.hasMore ? result.nextCursor : undefined;
    } while (cursor);

    return { data: allData, appliedIngestedEnd: appliedEnd };
  }
}
```

## Webhook Handler

```typescript theme={null}
import crypto from 'crypto';

type WebhookEvent =
  | 'consent.given'
  | 'consent.revoked'
  | 'consent.expiring'
  | 'consent.reauthorized'
  | 'data.ready'
  | 'data.failed';

interface WebhookPayload {
  event: WebhookEvent;
  timestamp: string;
  uid: string;
  client_id: string;
  sources: Array<Record<string, unknown>>;
}

class EmergeWebhookHandler {
  private secret: string;

  constructor(webhookSecret: string) {
    this.secret = webhookSecret;
  }

  /**
   * Verify webhook signature
   */
  verifySignature(payload: Buffer | string, signature: string): boolean {
    const expected = crypto
      .createHmac('sha256', this.secret)
      .update(payload)
      .digest('hex');

    const signatureBuf = Buffer.from(signature, 'hex');
    const expectedBuf = Buffer.from(expected, 'hex');

    if (signatureBuf.length !== expectedBuf.length) {
      return false;
    }

    return crypto.timingSafeEqual(signatureBuf, expectedBuf);
  }

  /**
   * Parse and verify webhook
   */
  parseWebhook(rawBody: Buffer, signature: string): WebhookPayload {
    if (!this.verifySignature(rawBody, signature)) {
      throw new Error('Invalid webhook signature');
    }

    return JSON.parse(rawBody.toString());
  }
}
```

## Usage Example

```typescript theme={null}
// Initialize clients
const linkClient = new EmergeLinkClient({
  clientId: process.env.EMERGE_CLIENT_ID!,
  signingSecret: process.env.EMERGE_SIGNING_SECRET!,
  apiToken: process.env.EMERGE_API_TOKEN!,
  redirectUri: 'https://yourapp.com/emerge/callback'
});

const queryClient = new EmergeQueryClient(process.env.EMERGE_API_TOKEN!);

// Create consent link
const { url, state } = linkClient.createLinkUrl({
  userId: 'psub_d4e5f6789012345678901234abcdef01'
});

// After consent, query data
const searchHistory = await queryClient.getSearch({
  uid: 'psub_d4e5f6789012345678901234abcdef01'
});

// Fetch all with pagination
const { data: allBrowsing, appliedIngestedEnd } = await queryClient.fetchAll(
  (params) => queryClient.getBrowsing(params),
  { uid: 'psub_d4e5f6789012345678901234abcdef01' }
);

// Delta sync - use appliedIngestedEnd for next sync
const nextSyncStart = appliedIngestedEnd;
```

## Express.js Integration

```typescript theme={null}
import express from 'express';

const app = express();

// Initialize clients
const linkClient = new EmergeLinkClient({
  clientId: process.env.EMERGE_CLIENT_ID!,
  signingSecret: process.env.EMERGE_SIGNING_SECRET!,
  apiToken: process.env.EMERGE_API_TOKEN!,
  redirectUri: 'https://yourapp.com/emerge/callback'
});

const webhookHandler = new EmergeWebhookHandler(
  process.env.EMERGE_WEBHOOK_SECRET!
);

// Create consent link
app.get('/connect', (req, res) => {
  const userId = req.session.userId;
  const { url, state } = linkClient.createLinkUrl({ userId });

  // Store state in session for verification
  req.session.emergeState = state;

  res.redirect(url);
});

// Handle callback
app.get('/emerge/callback', async (req, res) => {
  const params = linkClient.parseCallback(req.query as Record<string, string>);

  // Verify state
  const { valid } = linkClient.verifyState(params.state);
  if (!valid) {
    return res.status(400).send('Invalid state');
  }

  if (params.status === 'success' || params.status === 'reauthorized') {
    res.redirect('/dashboard?connected=true');
  } else {
    res.redirect(`/error?code=${params.errorCode}`);
  }
});

// Handle webhooks
app.post('/webhooks/emerge', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-signature'] as string;

  try {
    const payload = webhookHandler.parseWebhook(req.body, signature);

    switch (payload.event) {
      case 'consent.given':
        console.log(`Consent granted for user ${payload.uid}`);
        break;
      case 'consent.revoked':
        console.log(`Consent revoked for user ${payload.uid}`);
        break;
      case 'data.ready':
        console.log(`Data ready for user ${payload.uid}`, payload.sources);
        break;
      case 'data.failed':
        console.error(`Data export failed for user ${payload.uid}`, payload.sources);
        break;
      default:
        console.log(`Unhandled webhook event: ${payload.event}`);
    }

    res.status(200).send('OK');
  } catch (error) {
    res.status(401).send('Invalid signature');
  }
});
```
