Skip to main content
Every REST request must include both an API key and a signed JWT:
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:
ClaimValue
issnuvera-api
audnuvera-rest-api
subThe exact API key value sent in x-api-key
methodHTTP method, for example POST
uriExact path and query string, for example /api/v1/customers?limit=20
bodyHashSHA-256 hex hash of the exact request body bytes
iatCurrent Unix timestamp in seconds
expExpiration timestamp, no more than 60 seconds after iat
jtiUnique 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

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

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.
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.
A changed query string, body byte, multipart file name, or repeated jti will reject the request even when the API key is valid.