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

Requests to Transfers endpoints that move money must include a JSON Web Signature (JWS). JWS is a standard for digitally signing data to ensure its integrity and authenticity. Protected actions include [creating outbound transfers](/reference/transfers-api/transfers/transfers-create) and returning inbound transfers.

To sign an API call, follow these steps:

## Generate a pair of public and private JWS keys

Execute this command 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.pub
```

This generates two files:

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

Each file should look similar to this example:

```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-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDEbZHV0ODosbFn
BqcUYiRhlQWEJ9V9s5WfuFMv0n9hNWNFSBYiup+yuCNVW11vPQ7h6VUAWP8LFgmc
jsyjATlIsJDFDXXOQMyxAS2is2vZpot5QblcJMUxxMmAkUCJtpo7QSLKEPk2gRu4
tsQpzqnxCYDCnJOAHcClNrMIVJuczn/GMCm68IcAeNP8HQy9D20ER+w1tONoIzJ3
bQF0WkbtgJMYoQAq6UL3q0ws7ODvXo/ZcrFrY7q4skJopI+pz3qsGeSJX0PWjH4a
o+C9n5uYRXBxtH3nhnkSQdRZCk9fzId9+ObKrMKYuDxBeFqFIjZEh4pZ9lCPaHGK
qHwv3nLFAgMBAAECggEAHJnRpr7ryKX67UPoNw0VPAotS/Fa4hsweZmmrytotbhG
1JMq+fKPhz/NkUOk5qoOzTEi2dKbjDswuhWG0WM/uohPBAoyMY5433sK8IpMdVwN
KeI6gaKu/dCoAGrl6UdnzKHu1VpEVz3UUgB2rpmzX+/gyjVvOrPaVZQR3HApWlrr
qa/hmsiisCD1Bj7RreVAC6XfZCr8iyomNmX/Hz7Vx6s3HlBHzvQtxkwjjAPYu4SW
VsDAZESJNcOrZ8xNervaR3X+lLaF1DS/ntawPz6alAb46IyVgFU0K+rJn47MOTsi
gNSZqHV1TtTu7+jPkbF+LHHBYpYb4CSqyU41LKhDXwKBgQDk9smFRPjuSUiMGfI2
FCqwUmiYlAaEOHFLhvCapq2GfPjj1rIJMtqwox9fqKvHmTdiLjQ9+esyAbEG3CIW
njgsUsZspQOmCOqjuk0GsF9nsl/blzZrsytyN54xoRSECuecId6m31pQyJxoG1Y4
wqqAVObnepWb311xLIx/RI7NowKBgQDbn0WAuOv7JsXn/0CcZfzsbGRIMEkDLYe4
KiVOWALd6MijqtgyzRRwz2Qu7+AEfs0i4A0ZIOInZ2y5qkTeqpMNtqM9A4q5YClB
XTt/vUFxCLekL0s+zqXH9D87sTqEtTVNHpyJem3velXPDlSwR6MNyK0TRR7izBAg
mfYYSWV0dwKBgH6RfazSB9mRYS0xWpdSZpa5t2BA06lbmiVqHq8e3GWvx9YK5Lf5
CLMEOV+j2fGoXNlFOVPZR46JKNbl8WIXbG30BAQi4/VwkGSZo+LCtLqZ/CtjV44J
qUamQCinJrQnYwkIIBCW/1IQ04UpN2yBD8eJJ2tmdDWKMBlTywa/W0GJAoGBAJ12
WFKuQyNS7Vok/KIlzW2FWXEYjYClyEUWkqDVIVkRaalO+KuTtjAbweyVN7yBXXq/
wSRfG0a9NIr5tV8gVUbjx64bN/8pHusqeVpgyubMJT6mWgCyENKIID4gF6DGe2zL
odg/20p0H8nQsI+jDRj45H6IdFiPjpCRUoyfMwqJAoGBAJawh68qY9eQYWPwSq4g
S1RnrSmiamTm3zPqzeEZKcZOfrQz+W8q3Woq90HkupglXspL6bar6rZnuhcqY1xU
5m7IDq3fFRjfvsNDft5YJrtLFsJ/Uu6UHVnOa9K53aUJczMZ3CetqhJZmYvR9BPV
Yfo0J11kYXclnb1wH0svjZEA
-----END PRIVATE KEY-----
```

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

  Your JWS private key is sensitive and must remain confidential at all times. Fintoc will never ask for this key. If someone gains access to your private key, they could maliciously sign transfer requests on your behalf.
</Warning>

## Upload your public key to the dashboard

1. Go to [dashboard.fintoc.com](https://dashboard.fintoc.com)
2. Go to the API Keys tab on the side bar
3. If products that require JWS are active for your organization, you'll see a JWS public keys section. Click the **"Add JWS Key"** button and upload your JWS public key.

## Generate a signature

Now that you have uploaded your public key to the Fintoc dashboard, you can generate a signature with your private key. The signature ensures integrity and authenticity for each Transfers API call that moves money.

## Using our SDK

If you're using Python or Node, our [Python SDK](https://github.com/fintoc-com/fintoc-python) and [Node SDK](https://github.com/fintoc-com/fintoc-node) automatically generate the signatures for you. Initialize the Fintoc client with the `jws_private_key` argument, and the SDK signs each request for you:

<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);

  // You can now create transfers securely
  ```

  ```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'))

  # You can now create transfers securely
  ```
</CodeGroup>

## Step by step example

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 slight difference between the JSON used to create the JWS signature and the payload in your HTTP request may invalidate the signature.
</Info>

### Load the private key and configure headers

Load your private key from the PEM file and set up the JWS headers. Headers include the signing algorithm (`RS256`), a unique `nonce` to prevent duplicated signatures, the current `ts` timestamp, and the `crit` field specification.

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

  // define 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),            // timestamp of the request
    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 jws headers
  headers = {
      "alg": "RS256",                 # signing algorithm. must be "RS256"
      "nonce": secrets.token_hex(16), # unique string for each request
      "ts": int(time.time()),         # timestamp of the request
      "crit": ["ts", "nonce"]         # critical headers
  }
  ```
</CodeGroup>

#### Preventing a replay attack

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

Include a unique, random `nonce` in every request. This makes each signature distinct, even when the same data is sent multiple times. Fintoc accepts each `nonce` only once and rejects any request that reuses a `nonce`.

The `ts` timestamp records when the request was created, and Fintoc's servers validate that it falls within a 2-minute window to prevent the processing of 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

The signing input for a JWS consists of concatenating the base64url-encoded `headers` and the base64url-encoded `raw_body` with a period `.` between them, both without padding:

<CodeGroup>
  ```javascript Node theme={null}
  // generate the protected section of the jws by base64 encoding the headers without padding
  const protectedBase64 = Buffer.from(JSON.stringify(headers))
    .toString('base64url');

  // generate the payload section of the jws by base64 encoding the raw_body without padding
  const payloadBase64 = Buffer.from(rawBody)
    .toString('base64url');

  // generate the signature input by concatenating the encoded protected and payload components
  const signingInput = `${protectedBase64}.${payloadBase64}`;
  ```

  ```python Python theme={null}
  # generate the protected section of the jws by base64 encoding the headers without padding
  protected_base64 = base64.urlsafe_b64encode(
    json.dumps(headers).encode()
  ).rstrip(b'=').decode()

  # generate the payload section of the jws by base64 encoding the raw_body without padding
  payload_base64 = base64.urlsafe_b64encode(
    raw_body.encode()
  ).rstrip(b'=').decode()

  # generate the signature input by concatenating the encoded protected and payload components
  signing_input = f"{protected_base64}.{payload_base64}"
  ```
</CodeGroup>

### Generate the JWS token signature

Once you have the signing input ready, you'll create the cryptographic signature using your private key. The process involves:

1. Sign the signing input with your private key using RSA and PKCS1v15 padding.
2. Base64URL-encode the resulting signature, without padding.

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

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

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

  # base64 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 the JSON payload with different spacing, key ordering, or character encoding. To debug the signature, you can inspect the generated token at [https://jwt.io](https://jwt.io) to verify its content. We recommend confirming that the JWS token payload matches the `raw_body` sent in the HTTP request.

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

  const payload = Buffer.from(payloadBase64, 'base64url').toString()
  console.log(payload) // should be equal to raw_body 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 + '=' * (4 - len(payload_base64) % 4)
  )
  print(payload) # should be equal to 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>

## Complete example

Here is a complete example function that puts it all together. Install the imported libraries before you run it.

<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');

    // Debug output
    console.log(`Token: ${protectedBase64}.${payloadBase64}.${signatureBase64}`);
    console.log(`Payload: ${Buffer.from(payloadBase64, 'base64url').toString()}`);

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


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

  // signature that must be included in the 'Fintoc-JWS-Signature' request header
  const jwsSignatureHeader = generateJwsSignatureHeader(rawBody)
  ```

  ```python Python theme={null}
  import base64
  import json
  import time
  import secrets
  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}"

      # Create signature
      signature_raw = private_key.sign(
              signing_input.encode(),
              padding.PKCS1v15(),
              hashes.SHA256()
          )

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

      # Debug output
      print(f"Token: {protected_base64}.{payload_base64}.{signature_base64}")
      payload = base64.urlsafe_b64decode(
          payload_base64 + '=' * (4 - len(payload_base64) % 4)
      )
      print(f"Payload: {payload.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) # the exact payload to send in the HTTP request

  # signature that must be included 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](/reference/transfers-api/transfers/transfers-create) request and send the signature in the `Fintoc-JWS-Signature` header.

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

Fintoc returns `200 OK` and the created transfer when the signature is valid. A signature error or `401 Unauthorized` means the signed `raw_body` does not match the payload in the request body, the `nonce` was reused, or the `ts` timestamp falls outside the 2-minute window.
