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

# Signed Links

> Server-side authentication for web and custom integrations

# Signed Links

<Note>
  **Using the iOS SDK?** You don't need signed links. The SDK handles authentication automatically via `FeatKit.identify()`. This page is for web apps and custom integrations.
</Note>

Signed Links let you authenticate users from your backend without exposing credentials in client code. Use this approach when:

* Building a **web app** that opens the portal
* Creating a **custom integration** without the SDK
* Supporting **non-iOS platforms**
* Needing **server-controlled authentication**

## How it works

1. Your backend generates a signed URL with user info
2. Your app/website opens this URL
3. User is automatically authenticated on the portal
4. Votes and submissions are tied to their identity

## What you need

* **Project Slug** — from Project Settings → General
* **Secret Key** — from Project Settings → Signed Links
* **User info:** ID (required), email (optional), name (optional)

## Generating a Signed Link

Your backend creates a signed token with user data using HMAC-SHA256.

### Signature structure

```
Payload: { user_id, email, name, timestamp, expires }
Signature: HMAC-SHA256(base64(payload), secret_key)
Final URL: https://featkit.com/p/[slug]?token=[base64payload].[signature]
```

### Code examples

<CodeGroup>
  ```python Python theme={null}
  import hmac
  import hashlib
  import base64
  import json
  import time

  def generate_signed_link(slug, secret_key, user_id, email=None, name=None):
      payload = {
          "user_id": user_id,
          "email": email,
          "name": name,
          "timestamp": int(time.time()),
          "expires": int(time.time()) + 86400  # 24 hours
      }
      payload_json = json.dumps(payload, separators=(',', ':'))
      payload_b64 = base64.urlsafe_b64encode(payload_json.encode()).decode()
      signature = hmac.new(secret_key.encode(), payload_b64.encode(), hashlib.sha256).hexdigest()
      return f"https://featkit.com/p/{slug}?token={payload_b64}.{signature}"
  ```

  ```javascript Node.js theme={null}
  const crypto = require('crypto');

  function generateSignedLink(slug, secretKey, userId, email = null, name = null) {
    const payload = {
      user_id: userId,
      email: email,
      name: name,
      timestamp: Math.floor(Date.now() / 1000),
      expires: Math.floor(Date.now() / 1000) + 86400 // 24 hours
    };
    const payloadJson = JSON.stringify(payload);
    const payloadB64 = Buffer.from(payloadJson).toString('base64url');
    const signature = crypto.createHmac('sha256', secretKey).update(payloadB64).digest('hex');
    return `https://featkit.com/p/${slug}?token=${payloadB64}.${signature}`;
  }
  ```

  ```ruby Ruby theme={null}
  require 'openssl'
  require 'base64'
  require 'json'

  def generate_signed_link(slug, secret_key, user_id, email: nil, name: nil)
    payload = {
      user_id: user_id,
      email: email,
      name: name,
      timestamp: Time.now.to_i,
      expires: Time.now.to_i + 86400 # 24 hours
    }
    payload_json = payload.to_json
    payload_b64 = Base64.urlsafe_encode64(payload_json, padding: false)
    signature = OpenSSL::HMAC.hexdigest('sha256', secret_key, payload_b64)
    "https://featkit.com/p/#{slug}?token=#{payload_b64}.#{signature}"
  end
  ```
</CodeGroup>

## Using in a web app

```javascript theme={null}
// Frontend: Request signed URL from your backend
const response = await fetch('/api/featkit-link');
const { url } = await response.json();

// Open in new tab or redirect
window.open(url, '_blank');
```

```javascript theme={null}
// Backend (Express example)
app.get('/api/featkit-link', (req, res) => {
  const url = generateSignedLink(
    process.env.FEATKIT_SLUG,
    process.env.FEATKIT_SECRET,
    req.user.id,
    req.user.email,
    req.user.name
  );
  res.json({ url });
});
```

## Token expiration

Tokens should expire. Recommended: **24 hours**. Set the `expires` field to `timestamp + 86400`.

<Tip>
  Shorter expiration times (1-4 hours) are more secure but require more frequent token generation.
</Tip>

## Testing

Generate a test link in your dashboard:

**Project Settings → Signed Links → Test Link Generator**

This lets you verify your integration without deploying backend changes.

## Security notes

<Warning>
  **Never expose your Secret Key in client code.** Generate tokens on your backend only.
</Warning>

* Store your Secret Key securely (environment variables, secrets manager)
* Generate tokens server-side only
* Use HTTPS for all requests
* Set reasonable expiration times
