> ## 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.

# Create Signed Links

> Generate HMAC-signed URLs for the consent flow

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

| Parameter      | Type   | Description                                                     |
| -------------- | ------ | --------------------------------------------------------------- |
| `client_id`    | string | Your client identifier from the Control Room                    |
| `redirect_uri` | string | URL to redirect after consent                                   |
| `state`        | string | Random string for CSRF protection (you verify this in callback) |
| `timestamp`    | string | ISO 8601 timestamp (e.g., `2024-01-15T10:30:00.000Z`)           |
| `signature`    | string | HMAC-SHA256 signature of sorted parameters                      |

## Optional parameters

| Parameter     | Type   | Default | Description                                                                                                         |
| ------------- | ------ | ------- | ------------------------------------------------------------------------------------------------------------------- |
| `uid`         | string | -       | Your internal user id (server-side only). If omitted, Emerge generates a pairwise `uid` and returns it on callback. |
| `flow_config` | string | -       | Named configuration for custom branding                                                                             |

## Complete implementation

<Warning>
  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.
</Warning>

<CodeGroup>
  ```typescript TypeScript theme={null}
  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
    };
  }
  ```

  ```python Python theme={null}
  import hmac
  import hashlib
  import time
  import secrets
  from datetime import datetime, timezone
  from urllib.parse import urlencode
  from dataclasses import dataclass
  from typing import Optional

  @dataclass
  class LinkParams:
      client_id: str
      signing_secret: str
      redirect_uri: str
      user_id: Optional[str] = None


  def create_link_url(params: LinkParams) -> str:
      timestamp = datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z')
      state = secrets.token_hex(16)

      # Build parameters dict
      url_params = {
          'client_id': params.client_id,
          'redirect_uri': params.redirect_uri,
          'state': state,
          'timestamp': timestamp
      }

      if params.user_id:
          url_params['uid'] = params.user_id

      # Sort and create signature base (raw values, not URL-encoded)
      sorted_items = sorted(url_params.items())
      signature_base = '&'.join(f'{k}={v}' for k, v in sorted_items)

      # Generate signature
      signature = hmac.new(
          params.signing_secret.encode(),
          signature_base.encode(),
          hashlib.sha256
      ).hexdigest()

      # Build final URL
      url_params['signature'] = signature
      return f'https://link.emergedata.ai/link/start?{urlencode(url_params)}'


  # With state tracking for verification
  state_store: dict[str, dict] = {}


  def create_link_with_state_tracking(params: LinkParams) -> tuple[str, str]:
      timestamp = datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z')
      state = secrets.token_hex(16)

      # Store state for later verification
      state_store[state] = {
          'user_id': params.user_id or '',
          'created_at': time.time()
      }

      url_params = {
          'client_id': params.client_id,
          'redirect_uri': params.redirect_uri,
          'state': state,
          'timestamp': timestamp
      }

      if params.user_id:
          url_params['uid'] = params.user_id

      sorted_items = sorted(url_params.items())
      signature_base = '&'.join(f'{k}={v}' for k, v in sorted_items)

      signature = hmac.new(
          params.signing_secret.encode(),
          signature_base.encode(),
          hashlib.sha256
      ).hexdigest()

      url_params['signature'] = signature
      url = f'https://link.emergedata.ai/link/start?{urlencode(url_params)}'

      return url, state
  ```
</CodeGroup>

## Usage examples

### Basic link generation

```typescript theme={null}
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

```typescript theme={null}
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

```python theme={null}
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)
```

## Link validity

* 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

<Note>
  Generate links on-demand rather than storing them. This ensures the timestamp is always fresh.
</Note>

## 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

<Warning>
  The signature base uses **raw parameter values**, not URL-encoded values. URL encoding is only applied when constructing the final URL.
</Warning>
