> ## Documentation Index
> Fetch the complete documentation index at: https://docs.strata.io/llms.txt
> Use this file to discover all available pages before exploring further.

# API Clients

API clients are [confidential OAuth 2.0 clients](https://datatracker.ietf.org/doc/html/rfc6749#section-2.1) that use the [client credentials grant](https://datatracker.ietf.org/doc/html/rfc6749#section-4.4) to automate the Maverics platform via its public APIs. API clients are passwordless. Authentication is done via [JWT client assertions](https://datatracker.ietf.org/doc/html/rfc7523) signed with a private key.

## Prerequisites

* **Organization owner role** -- Required to create, edit, or revoke API clients and their key pairs.
* **HTTPS client** -- e.g., `curl` or any HTTP library.

## Create an API Client

<Steps>
  <Step title="Open the API Clients section">
    Click your profile in the upper-right corner of the Console, then click **Organizations**. Select the organization you want to manage. Locate the **API Clients** section on the organization page.
  </Step>

  <Step title="Create the client">
    Click **Create**. Enter a **Name** (required) and an optional **Description** (e.g., "CI deploy bot"). Click **Create** to save.
  </Step>

  <Step title="Add a key pair">
    With the API client settings open, click **Add Key Pair**. The Console generates a key pair on the server, displays the fingerprint, and prompts you to download the private key as a PEM file.
  </Step>

  <Step title="Download and store the private key">
    Click **Download Private Key**, then click **Done** to close the modal. Store the file securely -- treat it like any other production secret.
  </Step>
</Steps>

<Warning>
  The private key is delivered to your browser only at creation time and is not stored by Strata. If the file is lost, revoke the key pair and add a new one.
</Warning>

You can attach multiple key pairs to a single client to support rotation. Revoke an individual key pair from the API client settings without affecting others.

## Obtain an Access Token

API clients authenticate to the token endpoint using the OAuth 2.0 client credentials grant ([RFC 6749 §4.4](https://datatracker.ietf.org/doc/html/rfc6749#section-4.4)) with a JWT client assertion ([RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523)). The assertion proves possession of the client's private key without transmitting it.

### Discover the Token Endpoint

The auth endpoint is region-specific. Discover it from the OpenID Connect discovery document for your environment:

| Environment | Discovery document                                                  |
| ----------- | ------------------------------------------------------------------- |
| US          | `https://auth.us-east-2.strata.io/.well-known/openid-configuration` |
| UK          | `https://auth.eu-west-2.strata.io/.well-known/openid-configuration` |

Read the `token_endpoint` field from the discovery response and use its value as the destination for all token requests. Always take the endpoint from discovery rather than hardcoding the path, as it is the authoritative source.

### Build the Client Assertion

Construct a JWT signed with your client's private key using the `ES256` algorithm.

**JWS header:**

| Field | Value   |
| ----- | ------- |
| `alg` | `ES256` |
| `typ` | `JWT`   |

**JWT claims:**

| Claim | Description                                                    |
| ----- | -------------------------------------------------------------- |
| `iss` | Your API client ID (e.g., `client_<uuid>`).                    |
| `sub` | Your API client ID. Must equal `iss`.                          |
| `aud` | The token endpoint URL from discovery.                         |
| `exp` | Expiry timestamp (seconds since epoch). Must be in the future. |

<Tip>
  Inspect an [example assertion on jwt.io](https://jwt.io/#token=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJodHRwczovL2F1dGgudXMtZWFzdC0yLnN0cmF0YS5pby9vYXV0aDIvdG9rZW4iLCJleHAiOjE3NzkyMTA3MzUsImlhdCI6MTc3OTIxMDQzNSwiaXNzIjoiY2xpZW50XzAwMDAwMDAwLTAwMDAtMDAwMC0wMDAwLTAwMDAwMDAwMDAwMCIsInN1YiI6ImNsaWVudF8wMDAwMDAwMC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMDAwMDAifQ.ASt3TuaKS_IjicOhfuC4hEle8oBFr-SFJSksCkqvN8maH8ZbnEe3ayuNKRq9fxMzxvtxkZVvOpUjMNA_dI4n3g) with the placeholder header and claims pre-populated.
</Tip>

### Send the Token Request

POST the assertion to the token endpoint with form-encoded parameters:

```bash theme={null}
curl -X POST https://auth.<region>.strata.io/oauth2/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \
  -d "client_assertion=<signed JWT>"
```

A successful response returns an opaque bearer token:

```json theme={null}
{
  "access_token": "eyJhbGciOi...",
  "token_type": "Bearer",
  "expires_in": 3600
}
```

Include the token in the `Authorization` header for subsequent API calls:

```
Authorization: Bearer <access_token>
```

## Example code

Each tab builds a client assertion and exchanges it for an access token. Replace the placeholders with your client ID, private key path, and token endpoint.

<Tabs>
  <Tab title="Go">
    ```go theme={null}
    // Run:
    //   go get github.com/go-jose/go-jose/v4
    //   CLIENT_ID=<your-client-id> \
    //     KEY_PATH=client.pem \
    //     TOKEN_URL=https://auth.<region>.strata.io/oauth2/token \
    //     go run main.go
    package main

    import (
    	"crypto/ecdsa"
    	"crypto/x509"
    	"encoding/pem"
    	"fmt"
    	"io"
    	"net/http"
    	"net/url"
    	"os"
    	"strings"
    	"time"

    	"github.com/go-jose/go-jose/v4"
    	josejwt "github.com/go-jose/go-jose/v4/jwt"
    )

    func main() {
    	clientID := os.Getenv("CLIENT_ID")
    	keyPath := os.Getenv("KEY_PATH")
    	tokenURL := os.Getenv("TOKEN_URL")

    	pemBytes, _ := os.ReadFile(keyPath)
    	block, _ := pem.Decode(pemBytes)
    	raw, _ := x509.ParsePKCS8PrivateKey(block.Bytes)
    	key := raw.(*ecdsa.PrivateKey)

    	signer, _ := jose.NewSigner(
    		jose.SigningKey{Algorithm: jose.ES256, Key: key},
    		(&jose.SignerOptions{}).WithType("JWT"),
    	)

    	now := time.Now()
    	claims := josejwt.Claims{
    		Issuer:   clientID,
    		Subject:  clientID,
    		Audience: josejwt.Audience{tokenURL},
    		IssuedAt: josejwt.NewNumericDate(now),
    		Expiry:   josejwt.NewNumericDate(now.Add(5 * time.Minute)),
    	}

    	assertion, _ := josejwt.Signed(signer).Claims(claims).Serialize()

    	form := url.Values{
    		"grant_type":            {"client_credentials"},
    		"client_assertion_type": {"urn:ietf:params:oauth:client-assertion-type:jwt-bearer"},
    		"client_assertion":      {assertion},
    	}

    	resp, _ := http.Post(tokenURL, "application/x-www-form-urlencoded", strings.NewReader(form.Encode()))
    	defer resp.Body.Close()

    	// The response body contains the access token (JSON: access_token, token_type, expires_in).
    	body, _ := io.ReadAll(resp.Body)
    	fmt.Println("status:", resp.Status)
    	fmt.Println(string(body))
    }
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    # Run:
    #   pip install "PyJWT[crypto]" requests
    #   CLIENT_ID=<your-client-id> \
    #     KEY_PATH=client.pem \
    #     TOKEN_URL=https://auth.<region>.strata.io/oauth2/token \
    #     python main.py
    import os
    import time

    import jwt
    import requests

    CLIENT_ID = os.environ["CLIENT_ID"]
    KEY_PATH = os.environ["KEY_PATH"]
    TOKEN_URL = os.environ["TOKEN_URL"]

    with open(KEY_PATH, "rb") as f:
        private_key = f.read()

    now = int(time.time())
    claims = {
        "iss": CLIENT_ID,
        "sub": CLIENT_ID,
        "aud": TOKEN_URL,
        "iat": now,
        "exp": now + 300,
    }

    assertion = jwt.encode(claims, private_key, algorithm="ES256")

    resp = requests.post(
        TOKEN_URL,
        data={
            "grant_type": "client_credentials",
            "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
            "client_assertion": assertion,
        },
    )
    print(resp.status_code, resp.json())
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    // Run:
    //   npm install jose
    //   CLIENT_ID=<your-client-id> \
    //     KEY_PATH=client.pem \
    //     TOKEN_URL=https://auth.<region>.strata.io/oauth2/token \
    //     npx tsx main.ts
    import { readFile } from "node:fs/promises";
    import { SignJWT, importPKCS8 } from "jose";

    const CLIENT_ID = process.env.CLIENT_ID!;
    const KEY_PATH = process.env.KEY_PATH!;
    const TOKEN_URL = process.env.TOKEN_URL!;

    interface TokenResponse {
        access_token: string;
        token_type: string;
        expires_in: number;
    }

    const pem = await readFile(KEY_PATH, "utf8");
    const key = await importPKCS8(pem, "ES256");

    const assertion = await new SignJWT({})
        .setProtectedHeader({ alg: "ES256", typ: "JWT" })
        .setIssuer(CLIENT_ID)
        .setSubject(CLIENT_ID)
        .setAudience(TOKEN_URL)
        .setIssuedAt()
        .setExpirationTime("5m")
        .sign(key);

    const resp = await fetch(TOKEN_URL, {
        method: "POST",
        headers: { "Content-Type": "application/x-www-form-urlencoded" },
        body: new URLSearchParams({
            grant_type: "client_credentials",
            client_assertion_type:
                "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
            client_assertion: assertion,
        }),
    });
    const data = (await resp.json()) as TokenResponse;
    console.log(resp.status, data);
    ```
  </Tab>

  <Tab title="Rust">
    ```rust theme={null}
    // Run:
    //   Cargo.toml dependencies:
    //     jsonwebtoken = "9"
    //     reqwest = { version = "0.12", features = ["rustls-tls"] }
    //     serde = { version = "1", features = ["derive"] }
    //     tokio = { version = "1", features = ["full"] }
    //   CLIENT_ID=<your-client-id> \
    //     KEY_PATH=client.pem \
    //     TOKEN_URL=https://auth.<region>.strata.io/oauth2/token \
    //     cargo run --release
    use std::env;
    use std::fs;
    use std::time::{SystemTime, UNIX_EPOCH};

    use jsonwebtoken::{encode, Algorithm, EncodingKey, Header};
    use serde::Serialize;

    #[derive(Serialize)]
    struct Claims {
        iss: String,
        sub: String,
        aud: String,
        iat: u64,
        exp: u64,
    }

    #[tokio::main]
    async fn main() -> Result<(), Box<dyn std::error::Error>> {
        let client_id = env::var("CLIENT_ID")?;
        let key_path = env::var("KEY_PATH")?;
        let token_url = env::var("TOKEN_URL")?;

        let pem = fs::read(&key_path)?;
        let key = EncodingKey::from_ec_pem(&pem)?;

        let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
        let claims = Claims {
            iss: client_id.clone(),
            sub: client_id,
            aud: token_url.clone(),
            iat: now,
            exp: now + 300,
        };

        let mut header = Header::new(Algorithm::ES256);
        header.typ = Some("JWT".into());
        let assertion = encode(&header, &claims, &key)?;

        let resp = reqwest::Client::new()
            .post(&token_url)
            .form(&[
                ("grant_type", "client_credentials"),
                (
                    "client_assertion_type",
                    "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
                ),
                ("client_assertion", &assertion),
            ])
            .send()
            .await?;
        println!("{} {}", resp.status(), resp.text().await?);
        Ok(())
    }
    ```
  </Tab>
</Tabs>

## Common Errors

| Status | Error                                       | Likely Cause                                                                                                                                |
| ------ | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| 400    | `invalid_client` -- `invalid client`        | The `sub` claim does not match a registered API client in the requested region.                                                             |
| 400    | `invalid_client` -- `invalid JWT signature` | The signature does not verify against any registered public key for this client. Confirm you are signing with the matching private key.     |
| 400    | `invalid_client` -- `failed to parse JWT`   | The assertion is malformed. Verify it is a compact-serialized JWS with three base64url-encoded segments.                                    |
| 400    | `invalid_request`                           | Missing or malformed form parameter. Confirm `grant_type`, `client_assertion_type`, and `client_assertion` are all present and well-formed. |

## Rotate or Revoke

* **Rotate** -- Add a new key pair while the existing one is still active, switch your client to the new private key, then revoke the old key pair.
* **Revoke a key pair** -- Open the client drawer and click the revoke icon next to the key pair. Tokens issued before revocation remain valid until they expire.
* **Delete the client** -- Removes the client and all of its key pairs. The platform will begin rejecting new token requests for the deleted client.

## Related Pages

* [User Management](/reference/console/user-management) -- Organization roles required to administer API clients.
* [Audit Logs](/reference/console/audit-logs) -- API client lifecycle events appear in the audit log.
