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

# Request Signing

> How to sign API requests using JWT authentication

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

<Steps>
  <Step title="Prepare credentials">
    You need two credentials:

    * **API Key**: Your unique identifier
    * **Private Key**: Your RSA private key in PEM format
  </Step>

  <Step title="Extract URL path">
    Extract the path and query parameters from the request URL.
    Do not include the domain or protocol in the signed data.
  </Step>

  <Step title="Generate body hash">
    Calculate a SHA-256 hash of the request body to ensure integrity.
  </Step>

  <Step title="Create and sign JWT">
    Create a JWT with specific claims and sign it with your private key.
  </Step>

  <Step title="Add Authorization header">
    Include the signed JWT in the Authorization header as a Bearer token.
  </Step>
</Steps>

## Implementation

Below is a TypeScript implementation that you can use as a reference for other languages:

```typescript
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:

<ResponseField name="uri" type="string" required>
  The URL path with query parameters (excluding domain).
  Example: `/v1/resources?filter=active`
</ResponseField>

<ResponseField name="iat" type="number" required>
  Issued at time as a Unix timestamp (seconds since epoch).
</ResponseField>

<ResponseField name="exp" type="number" required>
  Expiration time as a Unix timestamp. Should be set to `iat + 55` seconds.
</ResponseField>

<ResponseField name="sub" type="string" required>
  Your API key.
</ResponseField>

<ResponseField name="bodyHash" type="string" required>
  SHA-256 hash of the request body as a hexadecimal string.
</ResponseField>

## Language-Specific Implementations

<Tabs>
  <Tab title="Node.js">
    ```javascript
    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}`;
    }
    ```
  </Tab>

  <Tab title="Python">
    ```python
    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}"
    ```
  </Tab>

  <Tab title="Java">
    ```java
    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);
    }
    ```
  </Tab>

  <Tab title="PHP">
    ```php
    <?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;
    }
    ?>
    ```
  </Tab>
</Tabs>

## Testing Your Implementation

You can verify your implementation is working correctly by:

1. Generating a signed request
2. Decoding the JWT at [jwt.io](https://jwt.io/) to check the claims
3. Making a test request to our API endpoint

<Warning>
  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.
</Warning>

## Common Issues

<AccordionGroup>
  <Accordion title="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)
  </Accordion>

  <Accordion title="Missing Required Claims">
    Make sure your JWT includes all required claims: `uri`, `iat`, `exp`, `sub`, and `bodyHash`.
  </Accordion>

  <Accordion title="Incorrect URL Format">
    The `uri` claim should only include the path and query string, not the domain or protocol.
  </Accordion>
</AccordionGroup>

## 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
