How HMAC Authentication Works
The core security pattern uses HMAC-SHA256 to sign requests to the Papermap API:- Concatenate
workspace_id+valid_untiltimestamp - Create HMAC-SHA256 hash using your secret key
- Send signature + timestamp in headers
- Papermap API verifies signature with the same secret
Authentication Flow
Your API → Generate HMAC Signature
payload = "workspace-456" + "1699999999"
signature = HMAC-SHA256(payload, secret_key)
Your API → Call Papermap Dashboard API
POST ${PAPERMAP_API_URL}/api/v1/external/dashboards
Headers:
X-API-Key-ID: your-api-key
X-Workspace-ID: workspace-456
X-Valid-Until: 1699999999
X-Signature: abc123...
Papermap API → Verifies Signature
- Extracts workspace_id and valid_until
- Computes: HMAC-SHA256(workspace_id + valid_until, secret_key)
- Compares computed signature with X-Signature header
- Checks valid_until hasn't expired
If the signature doesn’t match or the timestamp has expired, the request is
rejected with a 401 Unauthorized error.
Implementation by Language
import hmac
import hashlib
import time
def _create_signature(workspace_id: str, valid_until: str, secret_key: str) -> str:
"""Create HMAC signature for authentication"""
payload = f"{workspace_id}{valid_until}"
signature = hmac.new(
secret_key.encode(),
payload.encode(),
hashlib.sha256
).hexdigest()
return signature
def _get_auth_headers(workspace_id: str, api_key_id: str, secret_key: str) -> dict:
"""Generate authentication headers with HMAC signature"""
valid_until = str(int(time.time()) + 300) # Valid for 5 minutes
signature = _create_signature(workspace_id, valid_until, secret_key)
return {
"X-API-Key-ID": api_key_id,
"X-Workspace-ID": workspace_id,
"X-Valid-Until": valid_until,
"X-Signature": signature,
"Content-Type": "application/json"
}
import crypto from "crypto";
class DashboardAPIService {
private config: {
apiKeyId: string;
secretKey: string;
workspaceId: string;
baseUrl: string;
};
constructor(config: any) {
this.config = config;
}
private createSignature(workspaceId: string, validUntil: string): string {
const payload = `${workspaceId}${validUntil}`;
return crypto
.createHmac("sha256", this.config.secretKey)
.update(payload)
.digest("hex");
}
private getAuthHeaders(): Record<string, string> {
const validUntil = String(Math.floor(Date.now() / 1000) + 300);
const signature = this.createSignature(this.config.workspaceId, validUntil);
return {
"X-API-Key-ID": this.config.apiKeyId,
"X-Workspace-ID": this.config.workspaceId,
"X-Valid-Until": validUntil,
"X-Signature": signature,
"Content-Type": "application/json",
};
}
}
<?php
function createSignature($workspaceId, $validUntil, $secretKey) {
$payload = $workspaceId . $validUntil;
return hash_hmac('sha256', $payload, $secretKey);
}
function getAuthHeaders($workspaceId, $apiKeyId, $secretKey) {
$validUntil = time() + 300; // Valid for 5 minutes
$signature = createSignature($workspaceId, (string)$validUntil, $secretKey);
return [
'X-API-Key-ID' => $apiKeyId,
'X-Workspace-ID' => $workspaceId,
'X-Valid-Until' => (string)$validUntil,
'X-Signature' => $signature,
'Content-Type' => 'application/json'
];
}
?>
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
@Service
public class DashboardAPIService {
@Value("${dashboard.api.key}")
private String apiKeyId;
@Value("${dashboard.secret.key}")
private String secretKey;
private String createSignature(String workspaceId, String validUntil)
throws Exception {
String payload = workspaceId + validUntil;
Mac hmac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKeySpec = new SecretKeySpec(
secretKey.getBytes(StandardCharsets.UTF_8),
"HmacSHA256"
);
hmac.init(secretKeySpec);
byte[] hash = hmac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
return bytesToHex(hash);
}
private HttpHeaders getAuthHeaders(String workspaceId) throws Exception {
long validUntil = Instant.now().getEpochSecond() + 300;
String signature = createSignature(workspaceId, String.valueOf(validUntil));
HttpHeaders headers = new HttpHeaders();
headers.set("X-API-Key-ID", apiKeyId);
headers.set("X-Workspace-ID", workspaceId);
headers.set("X-Valid-Until", String.valueOf(validUntil));
headers.set("X-Signature", signature);
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
private String bytesToHex(byte[] bytes) {
StringBuilder result = new StringBuilder();
for (byte b : bytes) {
result.append(String.format("%02x", b));
}
return result.toString();
}
}
using System;
using System.Security.Cryptography;
using System.Text;
public class DashboardAPIService
{
private readonly string _apiKeyId;
private readonly string _secretKey;
private readonly string _baseUrl;
public DashboardAPIService(string apiKeyId, string secretKey, string baseUrl)
{
_apiKeyId = apiKeyId;
_secretKey = secretKey;
_baseUrl = baseUrl;
}
private string CreateSignature(string workspaceId, string validUntil)
{
var payload = $"{workspaceId}{validUntil}";
var keyBytes = Encoding.UTF8.GetBytes(_secretKey);
var payloadBytes = Encoding.UTF8.GetBytes(payload);
using (var hmac = new HMACSHA256(keyBytes))
{
var hash = hmac.ComputeHash(payloadBytes);
return BitConverter.ToString(hash).Replace("-", "").ToLower();
}
}
private Dictionary<string, string> GetAuthHeaders(string workspaceId)
{
var validUntil = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + 300;
var signature = CreateSignature(workspaceId, validUntil.ToString());
return new Dictionary<string, string>
{
{ "X-API-Key-ID", _apiKeyId },
{ "X-Workspace-ID", workspaceId },
{ "X-Valid-Until", validUntil.ToString() },
{ "X-Signature", signature },
{ "Content-Type", "application/json" }
};
}
}
package dashboard
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"time"
)
type DashboardAPIService struct {
APIKeyID string
SecretKey string
BaseURL string
}
func (s *DashboardAPIService) createSignature(workspaceID, validUntil string) string {
payload := workspaceID + validUntil
h := hmac.New(sha256.New, []byte(s.SecretKey))
h.Write([]byte(payload))
return hex.EncodeToString(h.Sum(nil))
}
func (s *DashboardAPIService) getAuthHeaders(workspaceID string) map[string]string {
validUntil := fmt.Sprintf("%d", time.Now().Unix()+300)
signature := s.createSignature(workspaceID, validUntil)
return map[string]string{
"X-API-Key-ID": s.APIKeyID,
"X-Workspace-ID": workspaceID,
"X-Valid-Until": validUntil,
"X-Signature": signature,
"Content-Type": "application/json",
}
}
Token Expiration
- API requests: 5 minutes validity(advisable) based on the example above (
valid_until = now + 300) - Expired tokens are automatically rejected
Security Best Practices
Never hardcode API credentials: - Always use environment variables or a
secrets management system - The API endpoint URL should be treated as
sensitive configuration - Store credentials securely and rotate them regularly
Environment Configuration
Store your credentials securely:.env
PAPERMAP_API_KEY_ID=your-api-key-id
PAPERMAP_SECRET_KEY=your-secret-key-never-share
PAPERMAP_API_URL=<your-api-endpoint>
Obtaining Your Configuration: - API Credentials: Available in your
Papermap dashboard under Settings → API Keys - API Endpoint: Available
in Settings → API Configuration - Always use the values provided in your
dashboard for your specific workspace
Next Steps
Creating Dashboards
Learn how to create and manage dashboards for your tenants
Iframe Tokens
Generate secure tokens for embedding dashboards

