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

# API Authentication: JWT RS256 and RSA-4096 Key Setup

> Every Finrock API request requires a signed JWT and API key. Learn how to generate RSA keys, build the JWT payload, and sign requests correctly.

Every request to a protected Finrock endpoint must carry two headers: a short-lived JWT signed with your RSA-4096 private key, and a static API key issued from the control panel. Together these headers allow the platform to verify both your identity and the exact request you are making — including its body — so that replayed or tampered requests are rejected automatically.

## Required Headers

Include both of the following headers on every authenticated API call:

| Header          | Example                                | Description                                           |
| --------------- | -------------------------------------- | ----------------------------------------------------- |
| `Authorization` | `Bearer eyJhbGci...`                   | RS256-signed JWT built from the request payload       |
| `x-api-key`     | `4466c45a-7b28-4c50-a0f7-198f8d7f34c5` | UUID API key generated from the Finrock control panel |

<Note>
  Your API key is generated inside the Finrock control panel under **API Users**. Each API user has its own key and its own associated RSA public key that you upload during setup.
</Note>

***

## JWT Payload Fields

Build the JWT payload using the following fields before signing. Every field is required on every request.

<ParamField body="uri" type="string" required>
  The URI path of the request being made, without the domain. For example: `/dac/v1/transactions` or `/dac/v1/wallets`.
</ParamField>

<ParamField body="nonce" type="string" required>
  A unique value for this specific request. Use a UUID v4 or a cryptographically random string. **Never reuse a nonce** — the platform rejects requests with a previously seen nonce.
</ParamField>

<ParamField body="iat" type="integer" required>
  The time at which the JWT was issued, expressed as **seconds since the Unix Epoch** (e.g. `1715933300`). Use your system clock and ensure it is synchronised with NTP.
</ParamField>

<ParamField body="exp" type="integer" required>
  The expiry time of the JWT, expressed as **seconds since the Unix Epoch**. This value must be less than `iat + 30`. Tokens with a longer window are rejected with `401`.
</ParamField>

<ParamField body="sub" type="string" required>
  Your API key — the same UUID value you pass in the `x-api-key` header. For example: `4466c45a-7b28-4c50-a0f7-198f8d7f34c5`.
</ParamField>

<ParamField body="bodyHash" type="string" required>
  The hex-encoded SHA-256 hash of the **raw HTTP request body**. For GET requests with no body, hash an empty string. For POST requests, hash the raw JSON string exactly as it will be sent — before any encoding or transformation.
</ParamField>

<Warning>
  The `exp` field must be **strictly less than `iat + 30` seconds**. Any JWT with an expiry window greater than 30 seconds is rejected immediately with a `401 Unauthorized` response, regardless of signature validity.
</Warning>

***

## Computing the bodyHash

The `bodyHash` is the lowercase hex-encoded SHA-256 digest of the raw request body string. For requests with no body (such as GET requests), compute the hash of an empty string `""`.

**Example — POST request body:**

```json theme={null}
{"asset":"BTC","amount":0.0675,"type":"Withdraw","ts":1715933321398}
```

**Resulting SHA-256 bodyHash:**

```text theme={null}
6fd6a31980c78e97a5b1a12138b979934fb825f035c1ea2d3a8037a595bcedfb
```

<Tip>
  Hash the body string **before** you serialise or compress it further. The hash must match the exact bytes the server receives. Even a single extra space or different key order will produce a different hash and cause a `401`.
</Tip>

***

## Generating an RSA-4096 Key Pair

You need an RSA-4096 key pair to sign your JWTs. Generate one locally with OpenSSL, then upload the **public key** to the Finrock control panel when creating your API user. Keep the **private key** secret and never share it.

```shell theme={null}
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:4096 -out private_key.pem
openssl pkey -in private_key.pem -pubout -out public_key.pem
```

This produces two files:

* `private_key.pem` — used by your application to sign JWTs. **Never expose this file.**
* `public_key.pem` — uploaded to the Finrock control panel so the platform can verify your signatures.

***

## Signing the JWT — Code Examples

The examples below show how to build and sign the JWT payload in several languages using the RS256 algorithm. Full, runnable snippets are available at [github.com/gofinrock/jwt-snippets](https://github.com/gofinrock/jwt-snippets).

<CodeGroup>
  ```javascript Node.js theme={null}
  const fs = require("fs");
  const crypto = require("crypto");
  const jwt = require("jsonwebtoken"); // npm install jsonwebtoken

  const privateKey = fs.readFileSync("private_key.pem", "utf8");
  const apiKey = "4466c45a-7b28-4c50-a0f7-198f8d7f34c5";

  function buildAuthHeaders(uri, body = "") {
    const rawBody = body ? JSON.stringify(body) : "";
    const bodyHash = crypto
      .createHash("sha256")
      .update(rawBody)
      .digest("hex");

    const iat = Math.floor(Date.now() / 1000);
    const exp = iat + 29; // strictly less than iat + 30

    const payload = {
      uri,
      nonce: crypto.randomUUID(),
      iat,
      exp,
      sub: apiKey,
      bodyHash,
    };

    const token = jwt.sign(payload, privateKey, { algorithm: "RS256" });

    return {
      Authorization: `Bearer ${token}`,
      "x-api-key": apiKey,
      "Content-Type": "application/json",
    };
  }

  // Example: POST /dac/v1/transactions
  const headers = buildAuthHeaders("/dac/v1/transactions", {
    asset: "BTC",
    amount: 0.0675,
    type: "Withdraw",
    ts: Date.now(),
  });
  ```

  ```python Python theme={null}
  import json
  import time
  import uuid
  import hashlib

  import jwt  # pip install PyJWT cryptography

  with open("private_key.pem", "rb") as f:
      private_key = f.read()

  API_KEY = "4466c45a-7b28-4c50-a0f7-198f8d7f34c5"


  def build_auth_headers(uri: str, body: dict | None = None) -> dict:
      raw_body = json.dumps(body, separators=(",", ":")) if body else ""
      body_hash = hashlib.sha256(raw_body.encode()).hexdigest()

      iat = int(time.time())
      exp = iat + 29  # strictly less than iat + 30

      payload = {
          "uri": uri,
          "nonce": str(uuid.uuid4()),
          "iat": iat,
          "exp": exp,
          "sub": API_KEY,
          "bodyHash": body_hash,
      }

      token = jwt.encode(payload, private_key, algorithm="RS256")

      return {
          "Authorization": f"Bearer {token}",
          "x-api-key": API_KEY,
          "Content-Type": "application/json",
      }


  # Example: POST /dac/v1/transactions
  headers = build_auth_headers(
      "/dac/v1/transactions",
      {"asset": "BTC", "amount": 0.0675, "type": "Withdraw", "ts": int(time.time() * 1000)},
  )
  ```

  ```csharp C# theme={null}
  using System;
  using System.Security.Cryptography;
  using System.Text;
  using System.Text.Json;
  using Microsoft.IdentityModel.Tokens;
  using System.IdentityModel.Tokens.Jwt;
  // NuGet: Microsoft.IdentityModel.Tokens, System.IdentityModel.Tokens.Jwt

  public static class FinrockAuth
  {
      private const string ApiKey = "4466c45a-7b28-4c50-a0f7-198f8d7f34c5";

      public static (string Authorization, string XApiKey) BuildAuthHeaders(
          string uri,
          string rawBody = "")
      {
          // Compute bodyHash
          byte[] hashBytes = SHA256.HashData(Encoding.UTF8.GetBytes(rawBody));
          string bodyHash = Convert.ToHexString(hashBytes).ToLowerInvariant();

          long iat = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
          long exp = iat + 29; // strictly less than iat + 30

          // Load RSA private key
          using var rsa = RSA.Create();
          rsa.ImportFromPem(File.ReadAllText("private_key.pem"));

          var rsaKey = new RsaSecurityKey(rsa);
          var credentials = new SigningCredentials(rsaKey, SecurityAlgorithms.RsaSha256);

          var claims = new[]
          {
              new System.Security.Claims.Claim("uri", uri),
              new System.Security.Claims.Claim("nonce", Guid.NewGuid().ToString()),
              new System.Security.Claims.Claim("sub", ApiKey),
              new System.Security.Claims.Claim("bodyHash", bodyHash),
          };

          var tokenDescriptor = new SecurityTokenDescriptor
          {
              Subject = new System.Security.Claims.ClaimsIdentity(claims),
              IssuedAt = DateTimeOffset.FromUnixTimeSeconds(iat).UtcDateTime,
              Expires = DateTimeOffset.FromUnixTimeSeconds(exp).UtcDateTime,
              SigningCredentials = credentials,
          };

          var handler = new JwtSecurityTokenHandler();
          var token = handler.CreateEncodedJwt(tokenDescriptor);

          return ($"Bearer {token}", ApiKey);
      }
  }
  ```

  ```php PHP theme={null}
  <?php
  // composer require firebase/php-jwt
  use Firebase\JWT\JWT;

  $apiKey    = '4466c45a-7b28-4c50-a0f7-198f8d7f34c5';
  $privateKey = file_get_contents('private_key.pem');

  function buildAuthHeaders(string $uri, string $rawBody = ''): array
  {
      global $apiKey, $privateKey;

      $bodyHash = hash('sha256', $rawBody);

      $iat = time();
      $exp = $iat + 29; // strictly less than iat + 30

      $payload = [
          'uri'      => $uri,
          'nonce'    => bin2hex(random_bytes(16)),
          'iat'      => $iat,
          'exp'      => $exp,
          'sub'      => $apiKey,
          'bodyHash' => $bodyHash,
      ];

      $token = JWT::encode($payload, $privateKey, 'RS256');

      return [
          'Authorization' => "Bearer {$token}",
          'x-api-key'     => $apiKey,
          'Content-Type'  => 'application/json',
      ];
  }

  // Example: POST /dac/v1/transactions
  $body    = json_encode(['asset' => 'BTC', 'amount' => 0.0675,
                          'type' => 'Withdraw', 'ts' => round(microtime(true) * 1000)]);
  $headers = buildAuthHeaders('/dac/v1/transactions', $body);
  ?>
  ```
</CodeGroup>

***

## End-to-End Request Example

Once you have your signed JWT, attach both required headers to every API call.

```http theme={null}
POST /dac/v1/transactions HTTP/1.1
Host: api.finrock.io
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
x-api-key: 4466c45a-7b28-4c50-a0f7-198f8d7f34c5

{"asset":"BTC","amount":0.0675,"type":"Withdraw","ts":1715933321398}
```

***

## Common Authentication Errors

<Accordion title="401 Unauthorized — token expired or window too large">
  The JWT was rejected because either it has already expired, or the `exp` value is more than 30 seconds ahead of `iat`. Regenerate the JWT immediately before each request and keep the `exp` window at 29 seconds or fewer.
</Accordion>

<Accordion title="401 Unauthorized — invalid signature">
  The signature on the JWT could not be verified against the public key registered in the control panel. Confirm that you are signing with the private key that corresponds to the public key you uploaded, and that you are using the **RS256** algorithm (not HS256 or RS512).
</Accordion>

<Accordion title="401 Unauthorized — bodyHash mismatch">
  The hash in the JWT payload does not match the body the server received. Ensure you hash the raw JSON string **before** passing it to your HTTP client. Avoid pretty-printing or re-ordering keys between hashing and sending.
</Accordion>

<Accordion title="401 Unauthorized — nonce already used">
  The nonce value in this JWT has been seen before. Generate a fresh UUID or random string for every request — never cache or reuse a nonce.
</Accordion>
