Custom Backend Integration
Implement the signing endpoint manually for non-Next.js frameworks
If you're not using Next.js, you can implement the signing endpoint manually in any backend language.
How It Works
- Your frontend calls your backend's sign endpoint
- Your backend creates an HMAC-SHA256 signature
- Your backend returns signed data to the frontend
- The frontend sends this to fdback.io for authentication
Signature Format
The HMAC-SHA256 signature is computed over this message:
{timestamp}.{body}Where:
timestamp: Unix timestamp in milliseconds (e.g.,"1704067200000")body: JSON string of user data (compact, no extra spaces)
Common mistake: Do NOT include the workspace ID in the message. The format is just timestamp.body, not workspace_id.timestamp.body.
Required Body Fields
{
"email": "user@example.com",
"name": "John Doe",
"avatar": "https://example.com/avatar.jpg"
}| Field | Required | Description |
|---|---|---|
email | Yes | User's email address |
name | No | User's display name |
avatar | No | URL to user's avatar image |
Important: Do NOT include id, user_id, or any other fields. Only email, name, and avatar are accepted.
Response Format
Your endpoint must return this JSON structure:
{
"url": "https://app.fdback.io/api/auth/sdk-login",
"headers": {
"x-workspace-id": "your-workspace-id",
"x-timestamp": "1704067200000",
"x-signature": "a1b2c3d4e5f6..."
},
"body": "{\"email\":\"user@example.com\",\"name\":\"John Doe\"}",
"expiresAt": 1704067500000
}| Field | Description |
|---|---|
url | Always https://app.fdback.io/api/auth/sdk-login |
headers.x-workspace-id | Your workspace ID from the dashboard |
headers.x-timestamp | Unix timestamp in milliseconds when signed |
headers.x-signature | HMAC-SHA256 hex signature (64 characters) |
body | JSON string of user data (same as what was signed) |
expiresAt | Expiration timestamp (recommended: 5 minutes from now) |
Node.js / Express Example
const crypto = require('crypto');
const express = require('express');
const app = express();
app.use(express.json());
app.post('/api/fdback/sign', (req, res) => {
// 1. Get your authenticated user (from session, JWT, etc.)
const user = req.user; // Your auth system
if (!user?.email) {
return res.status(401).json({ error: 'Unauthorized' });
}
// 2. Create the body (email required, name/avatar optional)
const body = JSON.stringify({
email: user.email,
...(user.name && { name: user.name }),
...(user.avatar && { avatar: user.avatar }),
});
// 3. Create timestamp (milliseconds)
const timestamp = Date.now().toString();
// 4. Create signature: HMAC-SHA256 of "timestamp.body"
const message = `${timestamp}.${body}`;
const signature = crypto
.createHmac('sha256', process.env.FDBACK_WORKSPACE_SECRET)
.update(message)
.digest('hex');
// 5. Return the signed data
res.json({
url: 'https://app.fdback.io/api/auth/sdk-login',
headers: {
'x-workspace-id': process.env.FDBACK_WORKSPACE_ID,
'x-timestamp': timestamp,
'x-signature': signature,
},
body: body,
expiresAt: Date.now() + 5 * 60 * 1000, // 5 minutes
});
});Python / Flask Example
import hmac
import hashlib
import json
import time
from flask import Flask, jsonify
app = Flask(__name__)
FDBACK_WORKSPACE_ID = 'your-workspace-id'
FDBACK_WORKSPACE_SECRET = 'your-workspace-secret'
@app.route('/api/fdback/sign', methods=['POST'])
def sign_fdback():
# 1. Get your authenticated user
user = get_current_user() # Your auth system
if not user or not user.email:
return jsonify({'error': 'Unauthorized'}), 401
# 2. Create body (compact JSON, no extra spaces)
body_dict = {'email': user.email}
if user.name:
body_dict['name'] = user.name
if user.avatar:
body_dict['avatar'] = user.avatar
body = json.dumps(body_dict, separators=(',', ':'))
# 3. Create timestamp (milliseconds)
timestamp = str(int(time.time() * 1000))
# 4. Create signature
message = f"{timestamp}.{body}"
signature = hmac.new(
FDBACK_WORKSPACE_SECRET.encode('utf-8'),
message.encode('utf-8'),
hashlib.sha256
).hexdigest()
# 5. Return signed data
return jsonify({
'url': 'https://app.fdback.io/api/auth/sdk-login',
'headers': {
'x-workspace-id': FDBACK_WORKSPACE_ID,
'x-timestamp': timestamp,
'x-signature': signature,
},
'body': body,
'expiresAt': int(time.time() * 1000) + 5 * 60 * 1000,
})PHP Example
<?php
header('Content-Type: application/json');
// 1. Get your authenticated user
$user = get_current_user(); // Your auth system
if (!$user || !$user['email']) {
http_response_code(401);
echo json_encode(['error' => 'Unauthorized']);
exit;
}
// 2. Create body
$bodyArray = ['email' => $user['email']];
if (!empty($user['name'])) {
$bodyArray['name'] = $user['name'];
}
if (!empty($user['avatar'])) {
$bodyArray['avatar'] = $user['avatar'];
}
$body = json_encode($bodyArray, JSON_UNESCAPED_SLASHES);
// 3. Create timestamp (milliseconds)
$timestamp = (string)(time() * 1000);
// 4. Create signature
$message = $timestamp . '.' . $body;
$signature = hash_hmac('sha256', $message, $_ENV['FDBACK_WORKSPACE_SECRET']);
// 5. Return signed data
echo json_encode([
'url' => 'https://app.fdback.io/api/auth/sdk-login',
'headers' => [
'x-workspace-id' => $_ENV['FDBACK_WORKSPACE_ID'],
'x-timestamp' => $timestamp,
'x-signature' => $signature,
],
'body' => $body,
'expiresAt' => (time() * 1000) + (5 * 60 * 1000),
]);Ruby / Rails Example
require 'openssl'
require 'json'
class FdbackController < ApplicationController
def sign
# 1. Get your authenticated user
user = current_user
unless user&.email
return render json: { error: 'Unauthorized' }, status: :unauthorized
end
# 2. Create body
body_hash = { email: user.email }
body_hash[:name] = user.name if user.name.present?
body_hash[:avatar] = user.avatar if user.avatar.present?
body = body_hash.to_json
# 3. Create timestamp (milliseconds)
timestamp = (Time.now.to_f * 1000).to_i.to_s
# 4. Create signature
message = "#{timestamp}.#{body}"
signature = OpenSSL::HMAC.hexdigest(
'SHA256',
ENV['FDBACK_WORKSPACE_SECRET'],
message
)
# 5. Return signed data
render json: {
url: 'https://app.fdback.io/api/auth/sdk-login',
headers: {
'x-workspace-id': ENV['FDBACK_WORKSPACE_ID'],
'x-timestamp': timestamp,
'x-signature': signature
},
body: body,
expiresAt: (Time.now.to_f * 1000).to_i + (5 * 60 * 1000)
}
end
endTesting Your Signature
Use this script to verify your signature generation:
const crypto = require('crypto');
// Test data
const secret = 'your-workspace-secret';
const timestamp = '1704067200000';
const body = '{"email":"test@example.com","name":"Test User"}';
// Generate signature
const message = `${timestamp}.${body}`;
const signature = crypto
.createHmac('sha256', secret)
.update(message)
.digest('hex');
console.log('Message:', message);
console.log('Signature:', signature);
console.log('Signature length:', signature.length); // Should be 64Common Mistakes
| Mistake | Solution |
|---|---|
| Including workspace ID in message | Message is timestamp.body, NOT workspace_id.timestamp.body |
Adding id or user_id field | Only use email, name, avatar |
| Using seconds instead of milliseconds | Use Date.now() or time() * 1000 |
| Wrong URL in response | Must be https://app.fdback.io/api/auth/sdk-login |
| JSON with extra spaces | Use compact JSON (no spaces after : or ,) |
| Signature in base64 | Signature must be hex-encoded (64 characters) |
Security Notes
Never expose your workspace secret in client-side code. The secret should only exist on your server.
- Signatures expire after 5 minutes - Generate fresh data for each login attempt
- Use HTTPS for your sign endpoint
- Verify user authentication before signing - don't sign requests for unauthenticated users
- Use compact JSON - Ensure no extra whitespace in the body string