API clients are confidential OAuth 2.0 clients that use the client credentials grant to automate the Maverics platform via its public APIs. API clients are passwordless. Authentication is done via JWT client assertions 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
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.
Create the client
Click Create. Enter a Name (required) and an optional Description (e.g., “CI deploy bot”). Click Create to save.
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.
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.
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.
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) with a JWT client assertion (RFC 7523). 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:
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. |
Send the Token Request
POST the assertion to the token endpoint with form-encoded parameters:
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:
{
"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.
Go
Python
TypeScript
Rust
// 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))
}
# 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())
// 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);
// 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(())
}
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 — Organization roles required to administer API clients.
- Audit Logs — API client lifecycle events appear in the audit log.