Skip to main content
This guide walks you through collecting your first user consent and querying their data.

Connect your AI tools

Get AI-powered help while integrating by connecting these docs to your AI tools.
Add to .cursor/mcp.json:
{
  "mcpServers": {
    "Emerge": { "type": "http", "url": "https://docs.emergedata.ai/mcp" }
  }
}

Prerequisites

The Control Room account with API credentials
Don’t have an account? Sign up here.

Step 1: Configure your flow

Set up your company branding for the consent flow.
const response = await fetch('https://link.emergedata.ai/configs', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${API_TOKEN}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    config_name: 'default',
    company_name: 'Your Company',
    logo_url: 'https://yourcompany.com/logo.png',
    privacy_policy_url: 'https://yourcompany.com/privacy',
    is_default: true
  })
});
POST /configs is an upsert. Reusing the same config_name updates the existing config instead of returning a conflict, which makes setup scripts idempotent. Use GET /configs to list your configs and preview URLs.
Create an HMAC-signed URL that users click to start the consent flow.
Do not ask end-users for uid or collect it in the frontend. Use your internal user id server-side or omit uid and store the generated callback uid. This keeps user scoping private and avoids wrong-user data access.
import crypto from 'crypto';

function createLinkUrl(params: {
  clientId: string;
  signingSecret: string;
  redirectUri: string;
  userId: string;
}) {
  const timestamp = new Date().toISOString();
  const state = crypto.randomBytes(16).toString('hex');

  // Build parameters object
  const urlParams: Record<string, string> = {
    client_id: params.clientId,
    redirect_uri: params.redirectUri,
    state: state,
    timestamp: timestamp,
    uid: params.userId
  };

  // Sort parameters alphabetically for signature
  const sortedKeys = Object.keys(urlParams).sort();
  const signatureBase = sortedKeys
    .map(key => `${key}=${urlParams[key]}`)
    .join('&');

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

  // Build final URL with URL-encoded parameters
  const finalParams = new URLSearchParams(urlParams);
  finalParams.append('signature', signature);

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

// Usage
const linkUrl = createLinkUrl({
  clientId: 'your_client_id',
  signingSecret: 'your_signing_secret',
  redirectUri: 'https://yourcompany.com/callback',
  userId: 'psub_d4e5f6789012345678901234abcdef01'
});

Step 3: Handle the callback

After the user completes the consent flow, they’re redirected to your redirect_uri with these parameters:
ParameterDescription
statussuccess, reauthorized, or failure
stateThe state value you provided (verify this matches)
uidThe user identifier returned by Emerge (your value or a generated one)
error_codeOnly present if status=failure
// Express.js example
app.get('/callback', (req, res) => {
  const { status, state, uid, error_code } = req.query;

  // Verify state matches what you stored
  if (!verifyState(state)) {
    return res.status(400).send('Invalid state');
  }

  if (status === 'success' || status === 'reauthorized') {
    // Consent granted - data export will start automatically
    // Next step: poll /export/status/{uid} and wait for provider-level readiness
    console.log(`User ${uid} granted consent`);
    res.redirect('/success');
  } else {
    console.log(`Consent failed: ${error_code}`);
    res.redirect('/error');
  }
});

Step 4: Check export status

Before querying, poll GET /export/status/{uid} until the provider you need is ready.
async function waitForProviderExport(uid: string, provider: 'google_data' | 'gmail') {
  for (let attempt = 1; attempt <= 10; attempt += 1) {
    const response = await fetch(
      `https://link.emergedata.ai/export/status/${encodeURIComponent(uid)}`,
      {
        headers: {
          Authorization: `Bearer ${API_TOKEN}`
        }
      }
    );

    if (!response.ok) {
      const body = await response.text();
      throw new Error(`Export status failed (${response.status}): ${body}`);
    }

    const payload = await response.json() as {
      uid: string;
      sources: Array<{ provider: string; data_ready: boolean }>;
    };

    const source = payload.sources.find((item) => item.provider === provider);
    if (source?.data_ready) {
      return;
    }

    await new Promise((resolve) => setTimeout(resolve, 1500 * attempt));
  }

  throw new Error(`Export not ready for provider ${provider}`);
}

Response example

{
  "uid": "psub_d4e5f6789012345678901234abcdef01",
  "sources": [
    {
      "provider": "google_data",
      "data_ready": true,
      "data_landed_at": "2026-02-12T09:10:11Z",
      "export_status": "completed",
      "export_completed_at": "2026-02-12T09:10:11Z"
    },
    {
      "provider": "gmail",
      "data_ready": false,
      "data_landed_at": null,
      "export_status": "running",
      "export_completed_at": null
    }
  ]
}
You can also subscribe to data.ready and data.failed webhooks for near-real-time export updates. Keep polling GET /export/status/{uid} as a fallback for missed webhook deliveries.

Step 5: Query user data

Once consent is granted and data is exported, query it using the sync endpoints.
async function getUserSearchHistory(uid: string) {
  const response = await fetch(
    `https://query.emergedata.ai/v1/sync/get_search?uid=${uid}`,
    {
      headers: {
        'Authorization': `Bearer ${API_TOKEN}`
      }
    }
  );

  const data = await response.json();
  return data;
}

// Usage
const searchHistory = await getUserSearchHistory('psub_d4e5f6789012345678901234abcdef01');
console.log(searchHistory.data);
// [{ query: "best restaurants nearby", timestamp: "2024-01-15T10:30:00Z" }, ...]

Common mistakes to avoid

  • Using https://link.emergedata.ai/link instead of https://link.emergedata.ai/link/start
  • Signing URL parameters in the wrong order or signing URL-encoded values
  • Treating state as a user identifier or skipping state verification
  • Asking users to input uid or failing to store the callback uid
  • Querying before GET /export/status/{uid} shows data_ready: true for your provider
  • Exposing signing_secret or API tokens in client-side code

Next steps

Link Guide

Deep dive into consent flow options

Query Guide

Learn about sync vs async queries

Webhooks

Track consent and data export lifecycle changes

SDK Reference

Full SDK implementation examples