Skip to main content
Generate signed URLs server-side to initiate the consent flow. Each link is unique to a user and includes an HMAC signature for security.

URL structure

https://link.emergedata.ai/link/start?client_id=...&redirect_uri=...&state=...&timestamp=...&signature=...

Required parameters

ParameterTypeDescription
client_idstringYour client identifier from the Control Room
redirect_uristringURL to redirect after consent
statestringRandom string for CSRF protection (you verify this in callback)
timestampstringISO 8601 timestamp (e.g., 2024-01-15T10:30:00.000Z)
signaturestringHMAC-SHA256 signature of sorted parameters

Optional parameters

ParameterTypeDefaultDescription
uidstring-Your internal user id (server-side only). If omitted, Emerge generates a pairwise uid and returns it on callback.
flow_configstring-Named configuration for custom branding

Complete implementation

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 prevents wrong-user data access and keeps identifiers private.
import crypto from 'crypto';

interface LinkParams {
  clientId: string;
  signingSecret: string;
  redirectUri: string;
  userId?: string;
}

function createLinkUrl(params: LinkParams): 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
  };

  if (params.userId) {
    urlParams.uid = params.userId;
  }

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

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

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

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

// Store state for verification in callback
const stateStore = new Map<string, { userId: string; createdAt: number }>();

function createLinkWithStateTracking(params: LinkParams): {
  url: string;
  state: string;
} {
  const timestamp = new Date().toISOString();
  const state = crypto.randomBytes(16).toString('hex');

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

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

  if (params.userId) {
    urlParams.uid = params.userId;
  }

  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');

  const finalParams = new URLSearchParams(urlParams);
  finalParams.append('signature', signature);

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

Usage examples

const url = createLinkUrl({
  clientId: process.env.EMERGE_CLIENT_ID!,
  signingSecret: process.env.EMERGE_SIGNING_SECRET!,
  redirectUri: 'https://yourapp.com/emerge/callback',
  userId: 'psub_d4e5f6789012345678901234abcdef01'
});

// Redirect user to this URL or display as a button
console.log(url);

Express.js endpoint

import express from 'express';

const app = express();

app.get('/connect-data', (req, res) => {
  const userId = req.session.userId;

  const { url, state } = createLinkWithStateTracking({
    clientId: process.env.EMERGE_CLIENT_ID!,
    signingSecret: process.env.EMERGE_SIGNING_SECRET!,
    redirectUri: 'https://yourapp.com/emerge/callback',
    userId: userId
  });

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

  res.redirect(url);
});

FastAPI endpoint

from fastapi import FastAPI, Request
from fastapi.responses import RedirectResponse
import os

app = FastAPI()

@app.get('/connect-data')
async def connect_data(request: Request):
    user_id = request.session.get('user_id')

    url, state = create_link_with_state_tracking(LinkParams(
        client_id=os.environ['EMERGE_CLIENT_ID'],
        signing_secret=os.environ['EMERGE_SIGNING_SECRET'],
        redirect_uri='https://yourapp.com/emerge/callback',
        user_id=user_id
    ))

    # Store state in session for verification
    request.session['emerge_state'] = state

    return RedirectResponse(url)
  • Links are valid for 30 days from the timestamp
  • The same link parameters generate the same session (idempotent)
  • Expired links show an error page to users
Generate links on-demand rather than storing them. This ensures the timestamp is always fresh.

Signature algorithm

The signature ensures the link hasn’t been tampered with:
  1. Collect all URL parameters (except signature)
  2. Sort parameters alphabetically by key
  3. Join as key=value&key=value (raw values, not URL-encoded)
  4. Generate HMAC-SHA256 using your signing secret
  5. Append signature as hex string
The signature base uses raw parameter values, not URL-encoded values. URL encoding is only applied when constructing the final URL.