> For the complete documentation index, see [llms.txt](https://verbatim-ai.gitbook.io/docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://verbatim-ai.gitbook.io/docs/integration/rsa-keys.md).

# RSA Keys User Guide

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](/docs/integration/security.md).

⚠️ The [GitHub TokenBuilder](https://github.com/verbatim-ai/python-token-builder) project documents the RSA key generation process. You will also find code there to help you generate your keys.

> <https://github.com/verbatim-ai/python-token-builder>

***

## How it works

1. You generate an RSA key pair.
2. You register the **public** key in the backoffice. Verbatim stores it and gives it a **key id** (a UUID).
3. Your backend signs a JWT (**RS512**) with the **private** key and puts the key id in the JWT's `kid` header.
4. 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` 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+**
* **`openssl`** available on your `PATH` (used for key generation and RS512 signing)

The script uses only the Python standard library — nothing to `pip install`.

### Install

⚠️ The [GitHub TokenBuilder](https://github.com/verbatim-ai/python-token-builder) project documents the RSA key generation process. You will also find code there to help you generate your keys.

> <https://github.com/verbatim-ai/python-token-builder>

```shell
uv sync
```

### Step 1 — Generate a key pair

By default keys are read from / written to the `../keys/` directory at the repo root ; override with `--keys-dir`.

```bash
python key-builder/main.py --gen-keys --key-name <your-key-name>
```

Example with a key named staging

```bash
python key-builder/main.py --gen-keys --key-name staging
```

This 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](https://verbatim-ai.gitbook.io/docs/api-keys). The lifecycle is:

By convention, use the same name value (, 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`):

```bash
# Basic token (org only)
python key-builder/main.py \
  --key-name staging \
  --key-id <key-uuid-from-backoffice> \
  --org-id <your-org-uuid>
```

```bash
# 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>
```

```bash
# 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
```

```bash
# 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> | pbcopy
```

The JWT is printed to **stdout**; all diagnostics go to **stderr**, so piping is clean.

You can verify your token with [jwt.io](https://www.jwt.io/), and get

as Header

```json
{
  "alg": "RS512",
  "typ": "JWT",
  "kid": "<your-key-uuid-from-backoffice>"
}
```

as Payload

```json
{
  "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.

```bash
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/whoami
```

You should get the json body

```json
{
  "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](https://www.verbatim-ai.com/api-docs/swagger/)

### 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](https://docs.astral.sh/uv/):

```bash
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**

```json
{ "alg": "RS512", "typ": "JWT", "kid": "<your-key-id>" }
```

**Payload**

```json
{
  "iss": "verbatim-ai.com",
  "iat": 1700000000,
  "exp": 1700003600,
  "oid": "<your-org-uuid>",
  "uid": "user_42"
}
```

* `iss` **must** be `verbatim-ai.com`.
* `oid` is required; `uid` is optional.
* `iat`/`exp` are 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:

1. `header_b64 = base64url(json(header))`
2. `payload_b64 = base64url(json(payload))`
3. `signing_input = header_b64 + "." + payload_b64`
4. `signature = RSA_SHA512_sign(private_key, signing_input)`
5. `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`)

```js
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`)

```python
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`)

```java
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);
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://verbatim-ai.gitbook.io/docs/integration/rsa-keys.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
