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:
| 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
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:
- Convert each form field value to a string.
- Sort repeated values for a field.
- Sort fields by name and value.
- For each file, include
fieldName, fileName, mimeType, size, and the SHA-256 hash of the file bytes.
- Sort files by field name, file name, size, and file SHA-256.
- 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.