# Request Signing Keys

> Request signing proves each request is authentic. It is mandatory in production and conditional in sandbox on your API key configuration.

## Overview

Request signing is a crucial security measure in the Noah Business API, ensuring the integrity and authenticity of each request. In the **`prod` environment**, request signing is **mandatory**, while in the **Sandbox environment**, it is conditionally required based on your API Key configuration.

Request signing involves constructing a JWT (JSON Web Token) that encapsulates specific claims about the request. This JWT is then signed using your **Request Signing Private Key** and included in the `Api-Signature` header of your API requests. Our API verifies this signature to authenticate and authorize the request.

## When is Request Signing Required?

- **Prod Environment:**
  - Request signing is compulsory for all API requests.
- **Sandbox Environment:**
  - Request signing is required only if the API Key was generated with an associated **Request Signing Public Key**. This facilitates easier testing with tools like Postman.

## Postman Usage

Import the generated collection:

- [Noah Business API Postman Collection](https://docs.noah.com/files/postman-collection.generated.json)

Then configure these variables before sending requests:

- `privateKey`: your Request Signing private key in PEM format
- `apiKey`: your API key (`X-Api-Key`)
- `baseUrl`: defaults to the sandbox URL; set to the value of `prodBaseUrl` for production

The collection includes a pre-request script that automatically creates `Api-Signature` for each request.

:::note Sandbox signing is optional

On sandbox, request signing is only required if your API Key was created with a Request Signing Public Key.
If your key was created without a signing public key, you can leave `privateKey` empty and send unsigned
requests using `apiKey` and `baseUrl` only. If an `Api-Signature` header is sent, it is always validated,
so an invalid signature can still fail on sandbox. On `prod`, signing is always required.

:::

The collection is generated from the public OpenAPI schema and updated automatically when that schema
changes on `main`.

## Constructing the JWT

To sign your requests, follow these steps:

1. **Create the JWT Payload:**
   - Include the necessary claims as outlined below.
2. **Sign the JWT:**
   - Use your **Request Signing Private Key** to sign the JWT with the ES384 algorithm. (ES256 can also be used but we recommend ES384)
3. **Include the JWT in the Header:**
   - Add the signed JWT to the `Api-Signature` header of your API request.

### JWT Claims

The following claims are verified by the Noah API:

| **Claim**     | **Description**                                                                                                 | **Required** |
| ------------- | --------------------------------------------------------------------------------------------------------------- | ------------ |
| `aud`         | The audience for which this JWT is intended, in all cases should be `https://api.noah.com`.                     | Yes          |
| `iat`         | Issued At time as a Unix timestamp in seconds.                                                                  | Yes          |
| `exp`         | Expiration time, must be within 15 minutes from `iat`.                                                          | Yes          |
| `method`      | HTTP method of the request (e.g., GET, POST).                                                                   | Yes          |
| `path`        | URL path of the request (e.g., `/v1/transactions`).                                                             | Yes          |
| `queryParams` | A map of query parameters. Must be included if the request uses query parameters; otherwise, omit.              | Conditional  |
| `bodyHash`    | Hex-encoded SHA256 hash of the exact request body. Must be included if the request has a body; otherwise, omit. | Conditional  |

**Example JWT Payload with Params:**

```json
{
  "aud": "https://api.noah.com",
  "iat": 1719820231,
  "exp": 1719820831,
  "method": "GET",
  "path": "/v1/transactions",
  "queryParams": {
    "PageSize": 20,
    "SortDirection": "ASC"
  }
}
```

**Example JWT Payload with Body Hash:**

```json
{
  "aud": "https://api.noah.com",
  "iat": 1719820231,
  "exp": 1719820831,
  "method": "POST",
  "path": "/v1/checkout/payin/fiat",
  "bodyHash": "96f9ab7b05d9ef0bdf3e46e6b83351c1fb2cf2675a5cb6b79f9bf9af390fb9b0"
}
```

## Request Signing Key Generation

If you require a new key, an ES384 private/public key pair can be generated using OpenSSL in command line as follows:

For ES384 (recommended):

```
openssl ecparam -name secp384r1 -genkey -noout -out private-key.pem
openssl ec -in private-key.pem -pubout -out public-key.pem
```

Only the public key is used when creating API Keys and can be shared as needed.

:::warning[Private Key Security]
Keep your private key secure. It provides full access to our APIs for initiating financial operations and must be protected against unauthorized access. Do not expose it in client-side code, repositories, or unencrypted storage.

**Do not share private keys between `sandbox` and `prod` environments**
:::

### Request Signing Example

The example functions below demonstrate how to make a simple api client using Axios where the `createJwt` function is responsible for generating a signed JSON Web Token (JWT) that authenticates your API requests.

#### Why Exact Bytes Matter

A critical aspect of the `createJwt` function is the hashing of the request body using SHA-256. It is imperative that the same byte sequence used to compute the hash is **exactly** what’s sent in your actual HTTP request payload. Any discrepancy, be it differences in encoding, whitespace, or data formatting, **will** result in a mismatched hash. A mismatch will lead to authentication failures, as our API relies on the hash to validate the request’s integrity. This verification enables us to ensure that the payload has not been tampered with during transit.

```ts
import crypto from 'crypto';
import type { AxiosResponse } from 'axios';
import axios from 'axios';
import jwt from 'jsonwebtoken';

const client = axios.create({ baseURL: 'https://api.sandbox.noah.com' });

/**
 * Creates a JWT token for authenticating API requests.
 *
 * @param opts - Options for JWT creation.
 * @param opts.body - A buffer made from the body of the request. Important to use the exact same body buffer in the request.
 * @param opts.method - The HTTP method of the request, e.g., GET, POST, PUT, DELETE.
 * @param opts.path - The path of the request, e.g., /api/v1/customers.
 * @param opts.privateKey - The private key used to sign the JWT, in PEM format.
 * @param opts.queryParams - The query parameters of the request.
 * @returns A signed JWT token as a string.
 */
export async function createJwt(opts: {
  body: Buffer | undefined;
  method: string;
  path: string;
  privateKey: string;
  queryParams: object | undefined;
}): Promise<string> {
  const { body, method, path, privateKey, queryParams } = opts;
  let bodyHash;

  if (body) {
    bodyHash = crypto.createHash('sha256').update(body).digest('hex');
  }

  const payload = {
    bodyHash,
    method,
    path,
    queryParams,
  };

  // ES384 is recommended but the algorithm can also be ES256
  const token = jwt.sign(payload, privateKey, {
    algorithm: 'ES384',
    audience: 'https://api.noah.com',
    // use a short expiry time, less than 15m
    expiresIn: '5m',
  });
  return token;
}

/**
 * Makes a GET request to the specified API endpoint with a signed JWT in the Api-Signature header.
 *
 * @param opts - Options for the GET request.
 * @param opts.path - The path of the request, e.g., /api/v1/customers.
 * @param opts.privateKey - The private key used to sign the JWT, in PEM format.
 * @param opts.queryParams - The query parameters of the request.
 * @returns A promise that resolves to the Axios response.
 * @throws Will throw an error if the request fails.
 */
export async function get(opts: {
  path: string;
  privateKey: string;
  queryParams?: Record<string, string | number>;
}): Promise<AxiosResponse> {
  const { path, privateKey, queryParams } = opts;

  try {
    const signature = await createJwt({
      body: undefined,
      method: 'GET',
      path,
      privateKey,
      queryParams,
    });

    const response = await client.get(path, {
      headers: { 'Api-Signature': signature },
      params: queryParams,
    });
    return response;
  } catch (err: unknown) {
    console.error(err);
    throw err;
  }
}

/**
 * Makes a POST request to the specified API endpoint with a signed JWT in the Api-Signature header.
 *
 * @param opts - Options for the POST request.
 * @param opts.data - The data to be sent in the body of the request.
 * @param opts.path - The path of the request, e.g., /api/v1/customers.
 * @param opts.privateKey - The private key used to sign the JWT, in PEM format.
 * @returns A promise that resolves to the Axios response.
 * @throws Will throw an error if the request fails.
 */
export async function post(opts: {
  data: object;
  path: string;
  privateKey: string;
}): Promise<AxiosResponse> {
  const { data, path, privateKey } = opts;

  try {
    const body = Buffer.from(JSON.stringify(data));
    const signature = await createJwt({
      body,
      method: 'POST',
      path,
      privateKey,
      queryParams: undefined,
    });

    const response = await client.post(path, body, {
      headers: { 'Api-Signature': signature, 'Content-Type': 'application/json' },
    });
    return response;
  } catch (err: unknown) {
    console.error(err);
    throw err;
  }
}

/**
 * Makes a PUT request to the specified API endpoint with a signed JWT in the Api-Signature header.
 *
 * @param opts - Options for the PUT request.
 * @param opts.data - The data to be sent in the body of the request.
 * @param opts.path - The path of the request, e.g., /api/v1/customers.
 * @param opts.privateKey - The private key used to sign the JWT, in PEM format.
 * @returns A promise that resolves to the Axios response.
 * @throws Will throw an error if the request fails.
 */
export async function put(opts: {
  data: object;
  path: string;
  privateKey: string;
}): Promise<AxiosResponse> {
  const { data, path, privateKey } = opts;

  try {
    const body = Buffer.from(JSON.stringify(data));
    const signature = await createJwt({
      body,
      method: 'PUT',
      path,
      privateKey,
      queryParams: undefined,
    });

    const response = await client.put(path, body, {
      headers: { 'Api-Signature': signature, 'Content-Type': 'application/json' },
    });
    return response;
  } catch (err: unknown) {
    console.error(err);
    throw err;
  }
}
```

#### Example Request Payloads

Given a `data` object as below, when we pass it to one of our `put` or `post` methods...

```ts
const data = {
  CryptoCurrency: 'USDC_TEST',
  CustomerID: 'noah-1235',
  ExternalID: 'noah-funds-124',
  FiatAmount: '100',
  FiatCurrency: 'USD',
  LineItems: [
    {
      Description: '100 USDT for use in noah.com',
      Quantity: '1',
      TotalAmount: '100',
      UnitAmount: '100',
    },
  ],
  Nonce: 'nonce-987654321',
  PaymentMethodCategory: 'Bank',
  ReturnURL: 'https://noah.com',
};
```

...then the resulting JSON body that needs to be used is shown below (note the lack of whitespace). It is critical that this exact body is used for your request payload. This is achieved in our functions above by using the same `Buffer` for the `createJwt` function and the `post` and `put` methods.

<!-- prettier-ignore -->
```json
{"CryptoCurrency":"USDC_TEST","CustomerID":"noah-1235","ExternalID":"noah-funds-124","FiatAmount":"100","FiatCurrency":"USD","LineItems":[{"Description":"100 USDT for use in noah.com","Quantity":"1","TotalAmount":"100","UnitAmount":"100"}],"Nonce":"nonce-987654321","PaymentMethodCategory":"Bank","ReturnURL":"https://noah.com"}
```

For reference, the `bodyHash` from the above will be:

```txt
a00dda342e76458acbe42035412ade81c784443fa9f0a01cc81a4ec6ab83a57c
```

:::tip

For details on the USDC_TEST token above, see [Sandbox Testnet Currencies](../../api-concepts/testnet-currencies.md).

:::
