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

# Generate JWS keys

> Sign Transfers API calls that move money with a JSON Web Signature.

Set up a public-private key pair to sign protected Transfers API requests. Transfers API requests that move money must include a JSON Web Signature (JWS). A JWS digitally signs data to ensure its integrity and authenticity. Protected actions include [creating outbound transfers](/api/transfers-api/transfers/transfers-create) and returning inbound transfers.

## Generate a public and private JWS key pair

Run these commands in your terminal:

```bash theme={null}
openssl genrsa -out private_key.pem 2048
openssl rsa -in private_key.pem -outform PEM -pubout -out public_key.pem
```

This generates two files:

* `private_key.pem`, containing your private key.
* `public_key.pem`, containing your public key.

The files use the following formats:

```text public_key.pem theme={null}
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoPPSwkMAHrLy6ZY+cOIP
jl6PxkrKJBicwMBMgFPf0Vtqe6QWepeOWXQuLgW+cSDI0KBjk8eZQEVB7GY3OwOl
DcknxUkaVueEvsDiY74xeC1iN2Gfb6HXd2JqgDWdWy/HNv2eUe9kmsSPSSgruA8Y
DvR6lpjPvAEJHP4Sg/B+9c0gBTDmqadL8UD291D7JbHmG4lIBT5NbhpOVnSBN0aC
R6ioxWz+VJoz68qsxHQ69TYhl8/jG79ocvZsZEWCWc/Kv7SP6/cPJHu0oGWVZwa4
5BtPLeMQ9ZleHdV6RCUbxFXKzbZF5fKQ+z5NWk+hMz5TCs4jwmg1nodWyW+bL7K9
YQIDAQAB
-----END PUBLIC KEY-----
```

```text private_key.pem theme={null}
-----BEGIN PRIVATE KEY-----
<redacted example private key content>
-----END PRIVATE KEY-----
```

<Warning>
  **Never share your JWS private key**

  Your JWS private key is sensitive and must remain confidential. Fintoc never asks for this key. Anyone with your private key can sign malicious transfer requests on your behalf.
</Warning>

## Upload your public key to the dashboard

1. Go to [dashboard.fintoc.com](https://dashboard.fintoc.com).
2. Open **API Keys** in the sidebar.
3. If your organization has products that require JWS, open **JWS Public Keys**, select **Add JWS Key**, and upload your JWS public key.

## Generate a signature

After you upload your public key to the Fintoc dashboard, generate signatures with your private key. Each signature protects the integrity and authenticity of a Transfers API call that moves money.

## Generate signatures with an SDK

The [Node SDK](https://github.com/fintoc-com/fintoc-node) and [Python SDK](https://github.com/fintoc-com/fintoc-python) generate signatures for you. Pass the private key when you initialize the Fintoc client. The SDK then signs each protected request:

<CodeGroup>
  ```javascript Node theme={null}
  const { Fintoc } = require('fintoc');

  // Provide a path to your PEM file
  const fintoc = new Fintoc('YOUR_API_KEY', 'private_key.pem');

  // Or pass the PEM key directly as a string
  // const fintoc = new Fintoc('YOUR_API_KEY', process.env.JWS_PRIVATE_KEY);

  // The client now signs protected transfer requests
  ```

  ```python Python theme={null}
  import os

  from fintoc import Fintoc

  # Provide a path to your PEM file
  client = Fintoc("YOUR_API_KEY", jws_private_key="private_key.pem")

  # Or pass the PEM key directly as a string
  # client = Fintoc("YOUR_API_KEY", jws_private_key=os.environ.get('JWS_PRIVATE_KEY'))

  # The client now signs protected transfer requests
  ```
</CodeGroup>

## Generate a signature manually

If you want to write your own implementation or use a different programming language, follow these steps:

### Prepare the payload

JWS signature generation operates on the exact JSON string sent in the HTTP request. Create that string by serializing your outbound transfer request body with your language's JSON serializer.

<CodeGroup>
  ```javascript Node theme={null}
  const body = {
    amount: 100000,
    currency: 'MXN',
    account_id: 'YOUR_ACCOUNT_ID',
    counterparty: {
      account_number: '000000000000000000'
    }
  };

  const rawBody = JSON.stringify(body);
  ```

  ```python Python theme={null}
  import json

  body = {
      "amount": 100000,
      "currency": "MXN",
      "account_id": "YOUR_ACCOUNT_ID",
      "counterparty": {
          "account_number": "000000000000000000"
      }
  }

  raw_body = json.dumps(body)
  ```
</CodeGroup>

<Info>
  **JSON string must be consistent**

  When serializing your request body to JSON, you must use the exact same string for two purposes:

  1. Creating the JWS signature
  2. Sending as the payload in your HTTP request

  Any difference between the JSON used to create the JWS signature and the HTTP request payload may invalidate the signature.
</Info>

### Load the private key and configure headers

Load your private key from the PEM file and configure the protected JWS header. The header includes the `RS256` signing algorithm, a unique `nonce`, and the current Unix timestamp in `ts`. The `crit` field identifies `ts` and `nonce` as critical fields.

<CodeGroup>
  ```javascript Node theme={null}
  // Load the private key
  const privateKey = readFileSync('./private_key.pem', 'utf8');

  // Define the JWS headers
  const headers = {
    alg: 'RS256',                                  // Signing algorithm. Must be "RS256"
    nonce: crypto.randomBytes(16).toString('hex'), // Unique string for each request
    ts: Math.floor(Date.now() / 1000),             // Unix timestamp in seconds
    crit: ['ts', 'nonce']                          // Critical headers
  };
  ```

  ```python Python theme={null}
  # Load the private key
  with open('./private_key.pem', 'rb') as f:
      private_key = serialization.load_pem_private_key(
          f.read(),
          password=None
      )

  # Define the JWS headers
  headers = {
      "alg": "RS256",                  # Signing algorithm. Must be "RS256"
      "nonce": secrets.token_hex(16),  # Unique string for each request
      "ts": int(time.time()),          # Unix timestamp in seconds
      "crit": ["ts", "nonce"]          # Critical headers
  }
  ```
</CodeGroup>

#### Prevent replay attacks

Fintoc uses the `nonce` and `ts` headers during JWS authentication to protect against [replay attacks](https://en.wikipedia.org/wiki/Replay_attack) and ensure request integrity.

Include a unique, random `nonce` in every request. The `nonce` makes each signature distinct, even when you send the same data multiple times. Fintoc accepts each `nonce` only once and rejects requests that reuse a `nonce`.

The Unix timestamp in `ts` records when you created the request. Fintoc validates that the timestamp falls within a 2-minute window and rejects outdated requests.

Together, `nonce` and `ts` prevent replays: Fintoc rejects any intercepted request that reuses a `nonce` or falls outside the timestamp window.

### Generate the signing input

Create the JWS signing input by joining the base64url-encoded `headers` and `raw_body` with a period (`.`). Encode both values without padding:

<CodeGroup>
  ```javascript Node theme={null}
  // Base64url-encode the protected JWS header without padding
  const protectedBase64 = Buffer.from(JSON.stringify(headers))
    .toString('base64url');

  // Base64url-encode rawBody without padding
  const payloadBase64 = Buffer.from(rawBody)
    .toString('base64url');

  // Join the encoded protected header and payload
  const signingInput = `${protectedBase64}.${payloadBase64}`;
  ```

  ```python Python theme={null}
  # Base64url-encode the protected JWS header without padding
  protected_base64 = base64.urlsafe_b64encode(
      json.dumps(headers).encode()
  ).rstrip(b'=').decode()

  # Base64url-encode raw_body without padding
  payload_base64 = base64.urlsafe_b64encode(
      raw_body.encode()
  ).rstrip(b'=').decode()

  # Join the encoded protected header and payload
  signing_input = f"{protected_base64}.{payload_base64}"
  ```
</CodeGroup>

### Generate the JWS token signature

Create the cryptographic signature from the signing input and your private key:

1. Sign the signing input with your private key using RSA and PKCS #1 v1.5 padding.
2. Base64url-encode the resulting signature without padding.

<CodeGroup>
  ```javascript Node theme={null}
  // Sign the signing input
  const signatureRaw = crypto.createSign('sha256')
    .update(signingInput)
    .sign({
      key: privateKey,
      padding: crypto.constants.RSA_PKCS1_PADDING
    });

  // Base64url-encode the raw signature without padding
  const signatureBase64 = Buffer.from(signatureRaw)
    .toString('base64url');
  ```

  ```python Python theme={null}
  # Sign the signing input
  signature_raw = private_key.sign(
      signing_input.encode(),
      padding.PKCS1v15(),
      hashes.SHA256()
  )

  # Base64url-encode the raw signature without padding
  signature_base64 = base64.urlsafe_b64encode(signature_raw).rstrip(b'=').decode()
  ```
</CodeGroup>

### Verify the JWS token (optional)

Some libraries re-encode JSON payloads with different spacing, key ordering, or character encoding. To debug a signature, decode the payload locally and confirm that it matches the `raw_body` sent in the HTTP request. Do not share a token that contains sensitive transfer data.

<CodeGroup>
  ```javascript Node theme={null}
  const token = `${protectedBase64}.${payloadBase64}.${signatureBase64}`;
  console.log(token);

  const payload = Buffer.from(payloadBase64, 'base64url').toString();
  console.log(payload); // Should equal rawBody sent in the HTTP request
  ```

  ```python Python theme={null}
  token = f"{protected_base64}.{payload_base64}.{signature_base64}"
  print(token)

  payload = base64.urlsafe_b64decode(
      payload_base64 + '=' * (-len(payload_base64) % 4)
  )
  print(payload.decode())  # Should equal raw_body sent in the HTTP request
  ```
</CodeGroup>

### Construct the `Fintoc-JWS-Signature` header

Construct the `Fintoc-JWS-Signature` header by concatenating the protected header and signature:

<CodeGroup>
  ```javascript Node theme={null}
  const jwsSignatureHeader = `${protectedBase64}.${signatureBase64}`;
  ```

  ```python Python theme={null}
  jws_signature_header = f"{protected_base64}.{signature_base64}"
  ```
</CodeGroup>

## Use the complete example

The following functions combine the manual signature steps. Install the Python `cryptography` package before running the Python example.

<CodeGroup>
  ```javascript Node theme={null}
  const crypto = require('crypto');
  const { readFileSync } = require('fs');

  function generateJwsSignatureHeader(rawBody) {
    // Read private key
    const privateKey = readFileSync('./private_key.pem', 'utf8');

    const headers = {
      alg: 'RS256',
      nonce: crypto.randomBytes(16).toString('hex'),
      ts: Math.floor(Date.now() / 1000),
      crit: ['ts', 'nonce']
    };

    const protectedBase64 = Buffer.from(JSON.stringify(headers))
      .toString('base64url');

    const payloadBase64 = Buffer.from(rawBody)
      .toString('base64url');

    const signingInput = `${protectedBase64}.${payloadBase64}`;
    const signatureRaw = crypto.createSign('sha256')
      .update(signingInput)
      .sign({
        key: privateKey,
        padding: crypto.constants.RSA_PKCS1_PADDING
      });

    const signatureBase64 = Buffer.from(signatureRaw)
      .toString('base64url');

    return `${protectedBase64}.${signatureBase64}`;
  }

  const payload = {
    amount: 100000,
    currency: 'MXN',
    account_id: 'YOUR_ACCOUNT_ID',
    counterparty: {
      account_number: '000000000000000000'
    }
  };
  const rawBody = JSON.stringify(payload); // Exact payload to send in the HTTP request

  // Signature to include in the 'Fintoc-JWS-Signature' request header
  const jwsSignatureHeader = generateJwsSignatureHeader(rawBody);
  ```

  ```python Python theme={null}
  import base64
  import json
  import secrets
  import time

  from cryptography.hazmat.primitives import hashes
  from cryptography.hazmat.primitives.asymmetric import padding
  from cryptography.hazmat.primitives import serialization


  def generate_jws_signature_header(raw_body):
      # Read private key
      with open('./private_key.pem', 'rb') as f:
          private_key = serialization.load_pem_private_key(
              f.read(),
              password=None
          )

      # Create headers
      headers = {
          'alg': 'RS256',
          'nonce': secrets.token_hex(16),
          'ts': int(time.time()),
          'crit': ['ts', 'nonce']
      }

      # Base64url-encode without padding
      protected_base64 = base64.urlsafe_b64encode(
          json.dumps(headers).encode()
      ).rstrip(b'=').decode()

      payload_base64 = base64.urlsafe_b64encode(
          raw_body.encode()
      ).rstrip(b'=').decode()
      signing_input = f"{protected_base64}.{payload_base64}"

      # Sign the signing input
      signature_raw = private_key.sign(
          signing_input.encode(),
          padding.PKCS1v15(),
          hashes.SHA256()
      )

      signature_base64 = base64.urlsafe_b64encode(signature_raw).rstrip(b'=').decode()

      return f"{protected_base64}.{signature_base64}"


  body = {
      "amount": 100000,
      "currency": "MXN",
      "account_id": "YOUR_ACCOUNT_ID",
      "counterparty": {
          "account_number": "000000000000000000"
      }
  }
  raw_body = json.dumps(body)  # Exact payload to send in the HTTP request

  # Signature to include in the 'Fintoc-JWS-Signature' request header
  jws_signature_header = generate_jws_signature_header(raw_body)
  ```
</CodeGroup>

## Test the integration

Confirm your signature works before you go live. Using a `test` mode API key, sign a [create outbound transfer](/api/transfers-api/transfers/transfers-create) request and send the signature in the `Fintoc-JWS-Signature` header.

1. Build the request body and serialize it with your JSON serializer.
2. Generate the signature from the serialized body with the function above.
3. Send the request with the `Fintoc-JWS-Signature` header set to the generated signature.

Fintoc returns `201 Created` and the created transfer when the signature is valid. A signature error or `401 Unauthorized` response can indicate a mismatched request body, a reused `nonce`, or a `ts` value outside the 2-minute window.
