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

# Request Signing

> Sign Nuvera REST API requests with x-api-key and an RS256 JWT

Every REST request must include both an API key and a signed JWT:

```http theme={null}
x-api-key: <your-api-key>
Authorization: Bearer <signed-request-jwt>
```

The API key identifies the API application. The JWT proves possession of the private key that matches the public key stored on the application, binds the token to the exact request, and prevents replay.

## JWT requirements

Use RS256 and include these claims:

| Claim      | Value                                                                 |
| ---------- | --------------------------------------------------------------------- |
| `iss`      | `nuvera-api`                                                          |
| `aud`      | `nuvera-rest-api`                                                     |
| `sub`      | The exact API key value sent in `x-api-key`                           |
| `method`   | HTTP method, for example `POST`                                       |
| `uri`      | Exact path and query string, for example `/api/v1/customers?limit=20` |
| `bodyHash` | SHA-256 hex hash of the exact request body bytes                      |
| `iat`      | Current Unix timestamp in seconds                                     |
| `exp`      | Expiration timestamp, no more than 60 seconds after `iat`             |
| `jti`      | Unique nonce for this request                                         |

For requests without a body, hash the empty byte string. For JSON requests, hash the exact JSON bytes sent to the API. Do not sign a pretty-printed body and send a minified body.

## Node.js example

```ts theme={null}
import { createHash, randomUUID } from "node:crypto";
import { SignJWT, importPKCS8 } from "jose";

export async function signNuveraRequest(input: {
  apiKey: string;
  privateKeyPem: string;
  method: string;
  url: string;
  rawBody?: string | Buffer;
}) {
  const parsedUrl = new URL(input.url);
  const uri = `${parsedUrl.pathname}${parsedUrl.search}`;
  const body = input.rawBody ?? Buffer.alloc(0);
  const bodyHash = createHash("sha256").update(body).digest("hex");
  const now = Math.floor(Date.now() / 1000);
  const privateKey = await importPKCS8(input.privateKeyPem, "RS256");

  return new SignJWT({
    method: input.method.toUpperCase(),
    uri,
    bodyHash,
    jti: randomUUID(),
  })
    .setProtectedHeader({ alg: "RS256", typ: "JWT" })
    .setIssuer("nuvera-api")
    .setAudience("nuvera-rest-api")
    .setSubject(input.apiKey)
    .setIssuedAt(now)
    .setExpirationTime(now + 55)
    .sign(privateKey);
}
```

## Shell and OpenSSL example

This example requires `jq`, `openssl`, and `xxd`.

```bash theme={null}
API_KEY="<your-api-key>"
METHOD="POST"
URL="https://api.nuvera.global/api/v1/customers"
URI="/api/v1/customers"
BODY='{"companyName":"Acme Imports","registrationNumber":"ACME-123","countryOfIncorporationId":"SG","businessIndustryId":"424350","documentIds":[],"persons":[],"legalEntityShareholders":[],"isDraft":true,"currentStep":1}'
NOW="$(date +%s)"
EXP="$((NOW + 55))"
JTI="$(uuidgen)"

BODY_HASH="$(printf '%s' "$BODY" | openssl dgst -sha256 -binary | xxd -p -c 256)"
HEADER="$(printf '{"alg":"RS256","typ":"JWT"}' | openssl base64 -A | tr '+/' '-_' | tr -d '=')"
PAYLOAD="$(
  jq -nc \
    --arg iss "nuvera-api" \
    --arg aud "nuvera-rest-api" \
    --arg sub "$API_KEY" \
    --arg method "$METHOD" \
    --arg uri "$URI" \
    --arg bodyHash "$BODY_HASH" \
    --arg jti "$JTI" \
    --argjson iat "$NOW" \
    --argjson exp "$EXP" \
    '{iss:$iss,aud:$aud,sub:$sub,method:$method,uri:$uri,bodyHash:$bodyHash,iat:$iat,exp:$exp,jti:$jti}'
)"
PAYLOAD_B64="$(printf '%s' "$PAYLOAD" | openssl base64 -A | tr '+/' '-_' | tr -d '=')"
SIGNING_INPUT="$HEADER.$PAYLOAD_B64"
SIGNATURE="$(printf '%s' "$SIGNING_INPUT" | openssl dgst -sha256 -sign ./private-key.pem -binary | openssl base64 -A | tr '+/' '-_' | tr -d '=')"
TOKEN="$SIGNING_INPUT.$SIGNATURE"

curl "$URL" \
  -H "x-api-key: $API_KEY" \
  -H "Authorization: Bearer $TOKEN" \
  -H "content-type: application/json" \
  --data "$BODY"
```

## Python example

```python theme={null}
import hashlib
import time
import uuid
from urllib.parse import urlparse

import jwt


def sign_nuvera_request(api_key: str, private_key_pem: str, method: str, url: str, raw_body: bytes | None = None) -> str:
    parsed = urlparse(url)
    uri = parsed.path + (f"?{parsed.query}" if parsed.query else "")
    body = raw_body or b""
    now = int(time.time())
    payload = {
        "iss": "nuvera-api",
        "aud": "nuvera-rest-api",
        "sub": api_key,
        "method": method.upper(),
        "uri": uri,
        "bodyHash": hashlib.sha256(body).hexdigest(),
        "iat": now,
        "exp": now + 55,
        "jti": str(uuid.uuid4()),
    }

    return jwt.encode(payload, private_key_pem, algorithm="RS256", headers={"typ": "JWT", "alg": "RS256"})
```

## cURL flow

Generate the JWT immediately before calling the endpoint. Use the same exact URL, method, and body bytes for signing and sending.

```bash theme={null}
BODY='{"companyName":"Acme Imports","registrationNumber":"ACME-123","countryOfIncorporationId":"SG","businessIndustryId":"424350","documentIds":[],"persons":[],"legalEntityShareholders":[],"isDraft":true,"currentStep":1}'
TOKEN="$(node ./sign-nuvera-request.mjs POST https://api.nuvera.global/api/v1/customers "$BODY")"

curl https://api.nuvera.global/api/v1/customers \
  -H "x-api-key: $NUVERA_API_KEY" \
  -H "Authorization: Bearer $TOKEN" \
  -H "content-type: application/json" \
  --data "$BODY"
```

## Multipart body hash

For multipart requests, the JWT `bodyHash` is not the raw multipart stream hash. Nuvera validates a canonical multipart hash after parsing fields and files:

1. Convert each form field value to a string.
2. Sort repeated values for a field.
3. Sort fields by name and value.
4. For each file, include `fieldName`, `fileName`, `mimeType`, `size`, and the SHA-256 hash of the file bytes.
5. Sort files by field name, file name, size, and file SHA-256.
6. Hash `JSON.stringify({ fields, files })`.

This is the same shape used in proof artifacts for multipart requests. Store field names, file names, MIME types, sizes, and file hashes in proofs, not raw file bytes.

<Warning>
  A changed query string, body byte, multipart file name, or repeated `jti` will reject the request even when the API key is valid.
</Warning>
