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

Prepare credentials

You need two credentials:
  • API Key: Your unique identifier
  • Private Key: Your RSA private key in PEM format
2

Extract URL path

Extract the path and query parameters from the request URL. Do not include the domain or protocol in the signed data.
3

Generate body hash

Calculate a SHA-256 hash of the request body to ensure integrity.
4

Create and sign JWT

Create a JWT with specific claims and sign it with your private key.
5

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:
uri
string
required
The URL path with query parameters (excluding domain). Example: /v1/resources?filter=active
iat
number
required
Issued at time as a Unix timestamp (seconds since epoch).
exp
number
required
Expiration time as a Unix timestamp. Should be set to iat + 55 seconds.
sub
string
required
Your API key.
bodyHash
string
required
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}`;
}

Testing Your Implementation

You can verify your implementation is working correctly by:
  1. Generating a signed request
  2. Decoding the JWT at jwt.io to check the claims
  3. 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

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