---
title: Private Key JWT
description: "Authenticate with ZITADEL's token introspection endpoint using Private Key JWT to validate access tokens"
sidebar_label: Private Key JWT
---
import IntrospectionResponse from './_introspection-response.mdx';
This guide explains how to use ZITADEL's [Private Key JWT authentication method](/apis/openidoauth/authn-methods#jwt-with-private-key) to authenticate your application and call the token introspection endpoint. This is not about securing an API, but about allowing your backend to introspect access tokens in a secure way.
If you're looking to set up Private Key JWT authentication specifically for Service Accounts, see our dedicated guide [here](/guides/integrate/service-accounts/private-key-jwt).
## Register an Application & Generate a Key Pair
To use Private Key JWT for introspection, you must register your client application in ZITADEL and create a key pair. This process enables your application to obtain credentials needed to generate JWT assertions for authentication.
1. In your ZITADEL project, click **New** to add an application.
2. Enter a name (e.g., "Backend Client") and select **API** as the application type.
3. Choose **JWT** as the authentication method and click **Continue**.
4. Review your settings and click **Create**.
5. After creation, you’ll see the application’s **Client ID**. There is no client secret—instead, authentication relies on your private key and JWT.
6. To generate a key pair, click **New** under the application’s keys section.
7. Select **JSON** as the key type, set an expiration if desired, and click **Add**.
8. Download and save the generated key by clicking **Download**. Afterward, click **Close**.
9. The downloaded key file will look like this:
```
{
"type": "application",
"keyId": "",
"key": "-----BEGIN RSA PRIVATE KEY-----\n\n-----END RSA PRIVATE KEY-----\n",
"appId": "",
"clientId": ""
}
```
10. In the left menu, select **URLs** to view the application’s OIDC endpoints. Note the **issuer** URL, **token_endpoint**, and **introspection_endpoint**.
11. Optionally, note your **Project ID** for reference.
## Calling the Introspection Endpoint
To introspect an access token in ZITADEL, your backend must authenticate by providing a JWT (`client_assertion`) signed with your application's private key. ZITADEL verifies this JWT to ensure the request is from a trusted application.
**Required request parameters:**
| **Parameter** | **Description** |
|------------------------- |---------------------------------------------------------------------------------------------------------------------|
| `client_assertion` | The JWT assertion created and signed as described below. |
| `client_assertion_type` | Must be `urn:ietf:params:oauth:client-assertion-type:jwt-bearer` |
| `token` | The access token you want to introspect. |
### Creating the Client Assertion (JWT)
The `client_assertion` parameter is a signed JWT with the following structure:
**Header:**
```json
{
"alg": "RS256",
"kid": ""
}
```
**Payload:**
```json
{
"iss": "",
"sub": "",
"aud": "https://${YOUR_DOMAIN}", // Your ZITADEL issuer URL
"exp": 1605183582, // Expiry (Unix timestamp)
"iat": 1605179982 // Issued at (Unix timestamp, not older than 1 hour)
}
```
- `iss` and `sub`: Your application’s Client ID.
- `aud`: The issuer URL for your ZITADEL instance (e.g. `https://my-tenant.zitadel.cloud`).
- `exp`: JWT expiration time (seconds since epoch).
- `iat`: JWT issued at time (seconds since epoch), must not be older than 1 hour.
You can generate the signed JWT using libraries in your preferred language or helper tools like [zitadel-tools](https://github.com/zitadel/zitadel-tools) or [https://dinochiesa.github.io/jwt/](https://dinochiesa.github.io/jwt/).
### Example Introspection HTTP Request
Make a POST request to the introspection endpoint, sending the above parameters:
```bash
curl --request POST \
--url ${YOUR_DOMAIN}/oauth/v2/introspect \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer \
--data client_assertion=eyJhbGciOiJSUzI1Ni... \
--data token=VjVxyCZmRmWYqd3_F5db9Pb9mHR
```
### Python Example
Below is a Python function that generates the JWT assertion and performs token introspection:
```python
def introspect_token(token_string):
# Build JWT for client assertion
payload = {
"iss": API_KEY_FILE["clientId"],
"sub": API_KEY_FILE["clientId"],
"aud": ZITADEL_ISSUER_URL,
"exp": int(time.time()) + 60 * 60, # expires in 1 hour
"iat": int(time.time())
}
headers = {
"alg": "RS256",
"kid": API_KEY_FILE["keyId"]
}
jwt_token = jwt.encode(payload, API_KEY_FILE["key"], algorithm="RS256", headers=headers)
req_headers = {"Content-Type": "application/x-www-form-urlencoded"}
req_data = {
"client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
"client_assertion": jwt_token,
"token": token_string
}
response = requests.post(ZITADEL_INTROSPECTION_ENDPOINT, headers=req_headers, data=req_data)
response.raise_for_status()
token_data = response.json()
print(f"Token data from introspection: {token_data}")
return token_data
```
## Introspection Response
For more details and a complete working example, see this [tutorial](https://github.com/zitadel/examples-api-access-and-token-introspection/tree/main/api-jwt) on registering an application in ZITADEL and calling the introspection endpoint with a JWT assertion.