Request Signing
Our API uses JWT-based request signing to authenticate and verify the integrity of your requests. This guide explains how to implement request signing in your applications.
Overview
Each request to our API must include a signed JWT token in the Authorization header. The token contains claims about the request and is signed using your private key with the RS256 algorithm.
Prepare credentials
You need two credentials:
API Key : Your unique identifier
Private Key : Your RSA private key in PEM format
Extract URL path
Extract the path and query parameters from the request URL.
Do not include the domain or protocol in the signed data.
Generate body hash
Calculate a SHA-256 hash of the request body to ensure integrity.
Create and sign JWT
Create a JWT with specific claims and sign it with your private key.
Add Authorization header
Include the signed JWT in the Authorization header as a Bearer token.
Implementation
Below is a TypeScript implementation that you can use as a reference for other languages:
import * as jwt from 'jsonwebtoken' ;
import * as crypto from 'crypto' ;
import { URL } from 'url' ;
/**
* Sign an API request with JWT using RS256 algorithm
* @param privateKey Your RSA private key
* @param apiKey Your API key
* @param requestUrl Full URL of the request
* @param requestBody Request body (string or undefined)
* @returns Bearer token string for Authorization header
*/
export function signRequest (
privateKey : string ,
apiKey : string ,
requestUrl : string ,
requestBody ?: string
) : string {
// Build request url - extract path and query
const parsedUrl = new URL ( requestUrl );
const url = parsedUrl . pathname + parsedUrl . search ;
// Hash request body
const body = requestBody || '{}' ;
const bodyHash = crypto
. createHash ( 'sha256' )
. update ( body )
. digest ( 'hex' );
// Generate the signed JWT
const jwtHeader = {
typ: 'JWT' ,
alg: 'RS256'
};
const now = Math . floor ( Date . now () / 1000 );
const jwtData = {
uri: url ,
iat: now ,
exp: now + 55 ,
sub: apiKey ,
bodyHash: bodyHash
};
const signedJwt = jwt . sign ( jwtData , privateKey , {
algorithm: 'RS256' ,
header: jwtHeader
});
// Return Authorization header value
return `Bearer ${ signedJwt } ` ;
}
JWT Payload Structure
The JWT payload must contain the following claims:
The URL path with query parameters (excluding domain).
Example: /v1/resources?filter=active
Issued at time as a Unix timestamp (seconds since epoch).
Expiration time as a Unix timestamp. Should be set to iat + 55 seconds.
SHA-256 hash of the request body as a hexadecimal string.
Language-Specific Implementations
const jwt = require ( 'jsonwebtoken' );
const crypto = require ( 'crypto' );
const { URL } = require ( 'url' );
function signRequest ( privateKey , apiKey , requestUrl , requestBody ) {
// Extract path with query
const parsedUrl = new URL ( requestUrl );
const url = parsedUrl . pathname + parsedUrl . search ;
// Hash body
const body = requestBody || '{}' ;
const bodyHash = crypto . createHash ( 'sha256' ). update ( body ). digest ( 'hex' );
// Create JWT
const now = Math . floor ( Date . now () / 1000 );
const payload = {
uri: url ,
iat: now ,
exp: now + 55 ,
sub: apiKey ,
bodyHash: bodyHash
};
const signedJwt = jwt . sign ( payload , privateKey , {
algorithm: 'RS256' ,
header: { typ: 'JWT' , alg: 'RS256' }
});
return `Bearer ${ signedJwt } ` ;
}
import time
import hashlib
from urllib.parse import urlparse
import jwt
def sign_request ( private_key , api_key , request_url , request_body = None ):
# Extract path with query
parsed_url = urlparse(request_url)
url = parsed_url.path
if parsed_url.query:
url += '?' + parsed_url.query
# Hash body
body = request_body if request_body else ' {} '
body_hash = hashlib.sha256(body.encode( 'utf-8' )).hexdigest()
# Create JWT
now = int (time.time())
payload = {
'uri' : url,
'iat' : now,
'exp' : now + 55 ,
'sub' : api_key,
'bodyHash' : body_hash
}
headers = {
'typ' : 'JWT' ,
'alg' : 'RS256'
}
signed_jwt = jwt.encode(
payload = payload,
key = private_key,
algorithm = 'RS256' ,
headers = headers
)
return f "Bearer { signed_jwt } "
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.MessageDigest;
import java.security.PrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.time.Instant;
import java.util.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public String signRequest ( String privateKeyPem, String apiKey, String requestUrl, String requestBody) throws Exception {
// Parse URL
URL url = new URL (requestUrl);
String path = url . getPath ();
String query = url . getQuery ();
String uriPath = query != null ? path + "?" + query : path;
// Hash body
String body = requestBody != null ? requestBody : "{}" ;
MessageDigest digest = MessageDigest . getInstance ( "SHA-256" );
byte [] hash = digest . digest ( body . getBytes ( StandardCharsets . UTF_8 ));
String bodyHash = bytesToHex (hash);
// Current time
long now = Instant . now (). getEpochSecond ();
// JWT header
Map < String , Object > header = new HashMap <>();
header . put ( "typ" , "JWT" );
header . put ( "alg" , "RS256" );
// JWT payload
Map < String , Object > claims = new HashMap <>();
claims . put ( "uri" , uriPath);
claims . put ( "iat" , now);
claims . put ( "exp" , now + 55 );
claims . put ( "sub" , apiKey);
claims . put ( "bodyHash" , bodyHash);
// Convert PEM to PrivateKey
PrivateKey privateKey = loadPrivateKey (privateKeyPem);
// Sign JWT
String signedJwt = Jwts . builder ()
. setHeader (header)
. setClaims (claims)
. signWith (privateKey, SignatureAlgorithm . RS256 )
. compact ();
return "Bearer " + signedJwt;
}
private static String bytesToHex ( byte [] hash) {
StringBuilder hexString = new StringBuilder ();
for ( byte b : hash) {
String hex = Integer . toHexString ( 0xff & b);
if ( hex . length () == 1 ) hexString . append ( '0' );
hexString . append (hex);
}
return hexString . toString ();
}
private static PrivateKey loadPrivateKey ( String privateKeyPem) throws Exception {
// Strip headers and convert to binary
String privateKeyContent = privateKeyPem
. replace ( "-----BEGIN PRIVATE KEY-----" , "" )
. replace ( "-----END PRIVATE KEY-----" , "" )
. replaceAll ( " \\ s" , "" );
byte [] encoded = Base64 . getDecoder (). decode (privateKeyContent);
// Create private key
KeyFactory keyFactory = KeyFactory . getInstance ( "RSA" );
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec (encoded);
return keyFactory . generatePrivate (keySpec);
}
<? php
use \Firebase\JWT\ JWT ;
function signRequest ( $privateKey , $apiKey , $requestUrl , $requestBody = null ) {
// Parse URL
$urlParts = parse_url ( $requestUrl );
$path = $urlParts [ 'path' ] ?? '' ;
$query = isset ( $urlParts [ 'query' ]) ? '?' . $urlParts [ 'query' ] : '' ;
$url = $path . $query ;
// Hash body
$body = $requestBody ?? '{}' ;
$bodyHash = hash ( 'sha256' , $body );
// Current time
$now = time ();
// JWT headers
$header = [
'typ' => 'JWT' ,
'alg' => 'RS256'
];
// JWT payload
$payload = [
'uri' => $url ,
'iat' => $now ,
'exp' => $now + 55 ,
'sub' => $apiKey ,
'bodyHash' => $bodyHash
];
// Sign JWT
$signedJwt = JWT :: encode ( $payload , $privateKey , 'RS256' , null , $header );
return 'Bearer ' . $signedJwt ;
}
?>
Testing Your Implementation
You can verify your implementation is working correctly by:
Generating a signed request
Decoding the JWT at jwt.io to check the claims
Making a test request to our API endpoint
Ensure your request body is identical between signature generation and the actual request.
Even a single whitespace difference will result in a different hash and signature validation failure.
Common Issues
Signature Verification Failed
This typically means:
Your private key format is incorrect
The body hash doesn’t match the actual request body
The JWT has expired (should only be valid for 55 seconds)
Make sure your JWT includes all required claims: uri, iat, exp, sub, and bodyHash.
The uri claim should only include the path and query string, not the domain or protocol.
Security Best Practices
Store your private key securely and never expose it in client-side code
Generate a new JWT for each request
Keep the expiration time short (55 seconds) to prevent replay attacks
Implement proper error handling for failed authentication