RSA Keys User Guide
Generate an RSA key pair, publish the public key and mint signed JWTs for server-to-server calls.
This guide shows how to authenticate server-to-server calls with an RSA-signed JWT: generate a key pair, publish the public key in the backoffice, and mint tokens — either with the bundled helper script scripts/build_keys.py, or from your own code in any language.
Golden rule: the private key signs tokens and never leaves your backend. Only the public key is uploaded to Verbatim. See Security best practices.
⚠️ The GitHub TokenBuilder project documents the RSA key generation process.
You will also find code there to help you generate your keys.
How it works
- You generate an RSA key pair.
- You register the public key in the backoffice. Verbatim stores it and gives it a key id (a UUID).
- Your backend signs a JWT (RS512) with the private key and puts the key id in the JWT's
kidheader. - On each request the server reads
kid, looks up your public key, verifies the signature, and trusts the claims (oid,uid, …).
Two identifiers — don't mix them up
| Name | What it is | Where it lives |
|---|---|---|
Key file name (--key-name) | The local filename of the key pair on disk. Used to sign. | Your backend only — never sent to the server. |
Key id (--key-id, the JWT kid) | The UUID Verbatim assigns when you register the public key. Used by the server to look up your key. | JWT kid header. |
They are deliberately independent: the local file can be named anything; the kid must be the id Verbatim returned. Keep a note of which file name maps to which key id.
Part A — Using the key-builder/main.py script
key-builder/main.py scriptkey-builder/main.py is a small, dependency-free helper that generates key pairs and mints tokens. It's the fastest way to get a working token for testing.
Requirements
- Python 3.10+
opensslavailable on yourPATH(used for key generation and RS512 signing)
The script uses only the Python standard library — nothing to pip install.
Install
⚠️ The GitHub TokenBuilder project documents the RSA key generation process.
You will also find code there to help you generate your keys.
uv syncStep 1 — Generate a key pair
By default keys are read from / written to the ../keys/ directory at the repo root ; override with --keys-dir.
python key-builder/main.py --gen-keys --key-name <your-key-name>Example with a key named staging
python key-builder/main.py --gen-keys --key-name stagingThis creates two files (RSA-4096) and prints the public key to your terminal:
| File | Purpose |
|---|---|
keys/<your-key-name> | Private key (chmod 600) — keep secret |
keys/<your-key-name>.pub | Public key — register this in the backoffice |
Only --key-name is required here; no key id exists yet.
Step 2 — Publish the public key in the backoffice
Register the contents of keys/staging.pub in the backoffice — follow Managing API keys. The lifecycle is:
By convention, use the same name value (<your-key-name>, like staging), in the backoffice.
Step 3 — Mint a JWT
Sign with the local private key (--key-name) and stamp the server key id into kid (--key-id):
# Basic token (org only)
python key-builder/main.py \
--key-name staging \
--key-id <key-uuid-from-backoffice> \
--org-id <your-org-uuid># Basic token (org only)
python key-builder/main.py \
--key-name staging \
--key-id <key-uuid-from-backoffice> \
--org-id <your-org-uuid-from-backoffice># Token that also carries an end-user id (uid claim)
python key-builder/main.py \
--key-name staging \
--key-id <your-key-uuid-from-backoffice> \
--org-id <your-org-uuid-from-backoffice> \
--user-id user_42# Copy straight to the clipboard (macOS)
python key-builder/main.py \
--key-name staging \
--key-id <your-key-uuid-from-backoffice> \
--org-id <your-org-uuid-from-backoffice> | pbcopyThe JWT is printed to stdout; all diagnostics go to stderr, so piping is clean.
You can verify your token with jwt.io, and get
as Header
{
"alg": "RS512",
"typ": "JWT",
"kid": "<your-key-uuid-from-backoffice>"
}as Payload
{
"iss": "verbatim-ai.com",
"iat": 1786020246,
"exp": 1786023846,
"oid": "<your-org-uuid-from-backoffice>",
"uid": "user_42"
}Step 4 — Test your setup
Test your setup immediately with the v1/auth/whoami endpoint.
JWT=$(python key-builder/main.py --key-name staging --key-id <key-uuid> --org-id <org-uuid>)
curl -H "Authorization: Bearer $JWT" \
https://api.verbatim-ai.com/v1/auth/whoamiYou should get the json body
{
"organizationId": "<your-org-uuid-from-backoffice>",
"userId": "user_42"
}In this example, you get a typical curl command authenticated by a JWT token, issued by your private RSA key.
You can use the JWT token to play with the APIs in your Swagger playground
Parameters
| Parameter | Required | Default | Description |
|---|---|---|---|
--key-name | yes | — | Local key-pair filename (keys/<name> + keys/<name>.pub). Signs; never sent to the server. |
--key-id | yes (minting) | — | UUID placed in the JWT kid header — the server key id from the backoffice. |
--org-id | yes (minting) | — | Organization UUID → JWT oid claim. |
--user-id | no | — | End-user id → JWT uid claim. |
--gen-keys | no | false | Generate a new key pair instead of minting (needs only --key-name). |
--keys-dir | no | ../keys | Directory for key files. |
--issuer | no | verbatim-ai.com | JWT iss claim. Must stay verbatim-ai.com for the server to accept it. |
--ttl | no | 3600 | Token lifetime in seconds. |
Running the tests
The script needs nothing installed, but the test suite uses uv:
uv sync # creates .venv with pytest + pytest-cov
uv run pytest # runs the suite; coverage must stay at or above 90%Coverage is printed to the terminal and written as HTML to htmlcov/index.html.
Part B — Minting a JWT from your own code
The script is convenient, but in production you'll sign tokens inside your backend. A Verbatim JWT is a standard RS512 JWT — any mature JWT library produces one. You only need to get three things right: the algorithm, the kid header, and the claims.
The exact token shape
Header
{ "alg": "RS512", "typ": "JWT", "kid": "<your-key-id>" }Payload
{
"iss": "verbatim-ai.com",
"iat": 1700000000,
"exp": 1700003600,
"oid": "<your-org-uuid>",
"uid": "user_42"
}issmust beverbatim-ai.com.oidis required;uidis optional.iat/expare Unix epoch seconds. Keep the window short (minutes to an hour).
Language-agnostic recipe
If you ever hand-roll it, a JWT is three base64url segments joined by dots:
header_b64 = base64url(json(header))payload_b64 = base64url(json(payload))signing_input = header_b64 + "." + payload_b64signature = RSA_SHA512_sign(private_key, signing_input)jwt = signing_input + "." + base64url(signature)
Use base64url without padding (drop trailing =). "RS512" = RSASSA-PKCS1-v1_5 with SHA-512. Prefer a library over hand-rolling — the examples below are one call each.
Node.js (jsonwebtoken)
jsonwebtoken)import fs from "node:fs";
import jwt from "jsonwebtoken";
const privateKey = fs.readFileSync("keys/staging", "utf8");
const token = jwt.sign(
{ oid: process.env.ORG_ID, uid: "user_42" },
privateKey,
{
algorithm: "RS512",
issuer: "verbatim-ai.com",
expiresIn: "1h",
keyid: process.env.KEY_ID, // -> kid header
}
);Python (PyJWT)
PyJWT)import jwt # pip install "pyjwt[crypto]"
with open("keys/staging") as f:
private_key = f.read()
token = jwt.encode(
{"iss": "verbatim-ai.com", "oid": ORG_ID, "uid": "user_42"},
private_key,
algorithm="RS512",
headers={"kid": KEY_ID},
)
# NB: PyJWT sets iat automatically; add "exp" for a bounded lifetime.Java (auth0 java-jwt)
auth0 java-jwt)Algorithm alg = Algorithm.RSA512(null, privateKey); // RSAPrivateKey
String token = JWT.create()
.withKeyId(keyId) // kid header
.withIssuer("verbatim-ai.com")
.withClaim("oid", orgId)
.withClaim("uid", "user_42")
.withIssuedAt(Instant.now())
.withExpiresAt(Instant.now().plusSeconds(3600))
.sign(alg);Updated about 1 hour ago