diff --git a/CLAUDE.md b/CLAUDE.md
index 6b75c5365..159a75897 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -10,10 +10,23 @@ When in doubt, follow the constitution exactly. Do not introduce new rules that
## Core Principles
-### Documentation-First Development
+### Documentation-first development
Every feature must include comprehensive, user-focused documentation. Documentation is not an afterthought but a first-class deliverable that guides implementation. All code changes require corresponding documentation updates.
+### Git Commits
+
+- Do NOT include `Co-Authored-By` lines in commit messages
+
+### SDK variable names (critical)
+
+> **CRITICAL**: Use the exact variable names below in all documentation and code examples.
+
+- Node.js: `scalekit`
+- Python: `scalekit_client`
+- Go: `scalekitClient`
+- Java: `scalekitClient`
+
### Multi-Language SDK Consistency
All code examples MUST include Node.js, Python, Go, and Java implementations with consistent variable naming conventions. Examples must show both success and error handling paths. Security implications must be explained for each implementation.
diff --git a/public/d2/docs/authenticate/m2m/api-keys-0.svg b/public/d2/docs/authenticate/m2m/api-keys-0.svg
new file mode 100644
index 000000000..e538bd561
--- /dev/null
+++ b/public/d2/docs/authenticate/m2m/api-keys-0.svg
@@ -0,0 +1,195 @@
+
diff --git a/src/configs/redirects.config.ts b/src/configs/redirects.config.ts
index 9f8d96d11..3826bcffb 100644
--- a/src/configs/redirects.config.ts
+++ b/src/configs/redirects.config.ts
@@ -273,7 +273,8 @@ export const redirects = {
'/m2m/authenticate-scalekit-api': '/guides/authenticate-scalekit-api/',
'/m2m/api-auth-for-m2m-clients': '/guides/m2m/api-auth-m2m-clients/',
'/m2m/external-ids-and-metadata': '/guides/external-ids-and-metadata/',
- '/m2m/scopes': '/guides/m2m/scopes/',
+ '/m2m/scopes': '/authenticate/m2m/api-auth-quickstart/',
+ '/guides/m2m/scopes/': '/authenticate/m2m/api-auth-quickstart/',
'/reference/api/m2m-clients': '/guides/m2m/api-auth-m2m-clients/',
// =============================================================================
diff --git a/src/configs/sidebar.config.ts b/src/configs/sidebar.config.ts
index 6bde2796e..d2a8b99ce 100644
--- a/src/configs/sidebar.config.ts
+++ b/src/configs/sidebar.config.ts
@@ -80,7 +80,7 @@ export const sidebar = [
items: [
// 'guides/m2m/overview', TODO: It uses M2M context, and older context. Hiding it until we are sure to open it up
'authenticate/m2m/api-auth-quickstart',
- 'guides/m2m/scopes',
+ 'authenticate/m2m/api-keys',
// 'guides/m2m/api-auth-m2m-clients', TODO: Translate this as guides for future
],
},
diff --git a/src/content/docs/authenticate/m2m/api-auth-quickstart.mdx b/src/content/docs/authenticate/m2m/api-auth-quickstart.mdx
index 1bc90ccb6..0476ccc90 100644
--- a/src/content/docs/authenticate/m2m/api-auth-quickstart.mdx
+++ b/src/content/docs/authenticate/m2m/api-auth-quickstart.mdx
@@ -1,8 +1,8 @@
---
-title: Add auth to your APIs
+title: Add OAuth 2.0 to your APIs
description: "Secure your APIs in minutes with OAuth 2.0 client credentials, scoped access, and JWT validation using Scalekit"
browseCentral:
- label: "API authentication with M2M OAuth"
+ label: "Secure APIs with OAuth"
filterType: ["tutorial"]
category: ["Auth modules"]
icon: book
@@ -19,17 +19,15 @@ head:
font-size: var(--sl-text-lg);
}
sidebar:
- label: "Quickstart: API Auth"
+ label: "Add OAuth 2.0"
---
import { LinkButton, Aside, Steps, Tabs, TabItem, Badge } from '@astrojs/starlight/components';
import InstallSDK from '@components/templates/_installsdk.mdx';
-APIs are a fundamental mechanism for systems to communicate and exchange data. By exposing API endpoints, applications enable customers, partners, and external systems to interact seamlessly, creating deeper integrations and increasing the _stickiness_ to your app and it's usecases.
+APIs let your customers, partners, and external systems interact with your application and its data. You need authentication to ensure only authorized clients can consume your APIs. Scalekit helps you add OAuth 2.0-based client-credentials authentication to your API endpoints.
-Exposing APIs without any authentication is like leaving your door wide open for anyone to walk in. Anyone can access your API endpoints, potentially causing security breaches, data leaks, and unauthorized usage.
-
-In this guide let's take a look at how your can add OAuth2.0 based client credentials authentication to your APIs to ensure only authorized clients can consume your APIs.
+Here's how it works:
```d2
shape: sequence_diagram
@@ -57,23 +55,26 @@ Your App -> API Client: 7. Returns the protected resource
```sh showLineNumbers=false
pip install scalekit-sdk-python
```
- Alternatively, you can use the REST APIs directly.
+ Alternatively, you can use the [REST APIs directly](/apis/#tag/api-auth).
2. ## Enable API client registration for your customers
- To enable secure API access, you must first register an application as an API client. This process generates unique credentials that will authenticate the client when interacting with your API.
+ Allow your customers to register their applications as API clients. This process generates unique credentials that they can use to authenticate their application when interacting with your API.
+
+ Scalekit will return a client ID and secret that you can show to your customers to integrate their application with your API.
+ - An Organization ID identifies your customer, and multiple API clients can be registered for the same organization.
+ - The `POST /organizations/{organization_id}/clients` endpoint creates a new API client for the organization. See [Scalekit API Authentication](/apis/#description/quickstart) to get the `` in case of HTTP requests.
- Upon registration, your app obtains a client ID and secret — essentially an application-level authentication mechanism. These credentials should be securely shared with your API client developers for integration (usually via view-once UI).
- ```bash title="POST /organizations/{organization_id}/clients"
+ ```bash title="POST /organizations/{organization_id}/clients" showLineNumbers=false
# For authentication details, see: http://docs.scalekit.com/apis#description/authentication
curl -L '/api/v1/organizations//clients' \
-H 'Content-Type: application/json' \
@@ -102,7 +103,10 @@ Your App -> API Client: 7. Returns the protected resource
}'
```
- ```json title="Sample response"
+
+ Sample response
+
+ ```json title="Sample response" showLineNumbers=false
{
"client": {
"client_id": "m2morg_68315758685323697",
@@ -142,6 +146,7 @@ Your App -> API Client: 7. Returns the protected resource
"plain_secret": "test_ly8G57h0ErRJSObJI6dShkoa..."
}
```
+
@@ -178,7 +183,10 @@ Your App -> API Client: 7. Returns the protected resource
plain_secret = response.plain_secret
```
- ```json title="Sample response"
+
+ Sample response
+
+ ```json title="Sample response" showLineNumbers=false
{
"client": {
"client_id": "m2morg_68315758685323697",
@@ -219,6 +227,7 @@ Your App -> API Client: 7. Returns the protected resource
}
```
+
@@ -355,4 +364,309 @@ Your App -> API Client: 7. Returns the protected resource
Upon successful token verification, your API server gains confidence in the request's legitimacy and can proceed to process the request, leveraging the authorization scopes embedded within the token.
+5. ## Register API client's scopes
+
+ Scopes are embedded in the access token and validated server-side using the Scalekit SDK. This ensures that API clients only access resources they're authorized for, adding an extra layer of security.
+
+ For example, you might create an API client for a customer's deployment service with scopes like `deploy:applications` and `read:deployments`.
+
+
+
+
+ ```bash title="Register an API client with specific scopes" wrap
+ curl -L 'https:///api/v1/organizations//clients' \
+ -H 'Content-Type: application/json' \
+ -H 'Authorization: Bearer ' \
+ -d '{
+ "name": "GitHub Actions Deployment Service",
+ "description": "Service account for GitHub Actions to deploy applications to production",
+ "scopes": [
+ "deploy:applications",
+ "read:deployments"
+ ],
+ "expiry": 3600
+ }'
+ ```
+
+ Sample response
+
+ ```json title="Sample response" showLineNumbers=false
+ {
+ "client": {
+ "client_id": "m2morg_68315758685323697",
+ "secrets": [
+ {
+ "id": "sks_68315758802764209",
+ "create_time": "2025-04-16T06:56:05.360Z",
+ "update_time": "2025-04-16T06:56:05.367190455Z",
+ "secret_suffix": "UZ0X",
+ "status": "ACTIVE",
+ "last_used_time": "2025-04-16T06:56:05.360Z"
+ }
+ ],
+ "name": "GitHub Actions Deployment Service",
+ "description": "Service account for GitHub Actions to deploy applications to production",
+ "organization_id": "org_59615193906282635",
+ "create_time": "2025-04-16T06:56:05.290Z",
+ "update_time": "2025-04-16T06:56:05.292145150Z",
+ "scopes": [
+ "deploy:applications",
+ "read:deployments"
+ ]
+ },
+ "plain_secret": ""
+ }
+ ```
+
+
+
+
+ ```javascript title="Register an API client with specific scopes"
+ // Use case: Your customer requests API access for their deployment automation.
+ // You register an API client app with the appropriate scopes.
+ import { ScalekitClient } from '@scalekit-sdk/node';
+
+ // Initialize Scalekit client (see installation guide for setup)
+ const scalekit = new ScalekitClient(
+ process.env.SCALEKIT_ENVIRONMENT_URL,
+ process.env.SCALEKIT_CLIENT_ID,
+ process.env.SCALEKIT_CLIENT_SECRET
+ );
+
+ async function createAPIClient() {
+ try {
+ // Define API client details with scopes your customer's app needs
+ const clientDetails = {
+ name: 'GitHub Actions Deployment Service',
+ description: 'Service account for GitHub Actions to deploy applications to production',
+ scopes: ['deploy:applications', 'read:deployments'],
+ expiry: 3600, // Token expiry in seconds
+ };
+
+ // API call to register the client
+ const response = await scalekit.m2m.createClient({
+ organizationId: process.env.SCALEKIT_ORGANIZATION_ID,
+ client: clientDetails,
+ });
+
+ // Response contains client details and the plain_secret (only returned once)
+ const clientId = response.client.client_id;
+ const plainSecret = response.plain_secret;
+
+ // Provide these credentials to your customer securely
+ console.log('Created API client:', clientId);
+ } catch (error) {
+ console.error('Error creating API client:', error);
+ }
+ }
+
+ createAPIClient();
+ ```
+
+
+
+
+ ```python title="Register an API client with specific scopes"
+ # Use case: Your customer requests API access for their deployment automation.
+ # You register an API client app with the appropriate scopes.
+ import os
+ from scalekit import ScalekitClient
+
+ # Initialize Scalekit client (see installation guide for setup)
+ scalekit_client = ScalekitClient(
+ env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"),
+ client_id=os.getenv("SCALEKIT_CLIENT_ID"),
+ client_secret=os.getenv("SCALEKIT_CLIENT_SECRET")
+ )
+
+ try:
+ # Define API client details with scopes your customer's app needs
+ from scalekit.v1.clients.clients_pb2 import OrganizationClient
+
+ client_details = OrganizationClient(
+ name="GitHub Actions Deployment Service",
+ description="Service account for GitHub Actions to deploy applications to production",
+ scopes=["deploy:applications", "read:deployments"],
+ expiry=3600 # Token expiry in seconds
+ )
+
+ # API call to register the client
+ response = scalekit_client.m2m_client.create_organization_client(
+ organization_id=os.getenv("SCALEKIT_ORGANIZATION_ID"),
+ m2m_client=client_details
+ )
+
+ # Response contains client details and the plain_secret (only returned once)
+ client_id = response.client.client_id
+ plain_secret = response.plain_secret
+
+ # Provide these credentials to your customer securely
+ print("Created API client:", client_id)
+
+ except Exception as e:
+ print("Error creating API client:", e)
+ ```
+
+
+
+
+ The API returns a JSON object with two key parts:
+ - `client.client_id` - The client identifier
+ - `plain_secret` - The client secret (only returned once, never stored by Scalekit)
+
+ Provide both values to your customer securely. Your customer will use these credentials in their application to authenticate with your API. The `plain_secret` is never shown again after creation.
+
+
+
+6. ## Verify API client's scopes
+
+ When your API server receives a request from an API client app, you must validate the scopes present in the access token provided in the `Authorization` header. The access token is a JSON Web Token (JWT).
+
+ First, let's look at the claims inside a decoded JWT payload. Scalekit encodes the granted scopes in the `scopes` field.
+
+ ```json title="Example decoded access token" showLineNumbers=false {9-12}
+ {
+ "client_id": "m2morg_69038819013296423",
+ "exp": 1745305340,
+ "iat": 1745218940,
+ "iss": "",
+ "jti": "tkn_69041163914445100",
+ "nbf": 1745218940,
+ "oid": "org_59615193906282635",
+ "scopes": [
+ "deploy:applications",
+ "read:deployments"
+ ],
+ "sub": "m2morg_69038819013296423"
+ }
+ ```
+
+
+
+ Your API server should inspect the `scopes` array in the token payload to authorize the requested operation. Here's how you validate the token and check for a specific scope in your API server.
+
+
+
+
+ ```javascript title="Example Express.js middleware for scope validation" collapse={1-27}
+ // Security: ALWAYS validate the access token on your server before trusting its claims.
+ // This prevents token forgery and ensures the token has not expired.
+ import { ScalekitClient } from '@scalekit-sdk/node';
+ import jwt from 'jsonwebtoken';
+ import jwksClient from 'jwks-rsa';
+
+ const scalekit = new ScalekitClient(
+ process.env.SCALEKIT_ENVIRONMENT_URL,
+ process.env.SCALEKIT_CLIENT_ID,
+ process.env.SCALEKIT_CLIENT_SECRET
+ );
+
+ // Setup JWKS client for token verification
+ const client = jwksClient({
+ jwksUri: `${process.env.SCALEKIT_ENVIRONMENT_URL}/.well-known/jwks.json`,
+ cache: true
+ });
+
+ async function getPublicKey(header) {
+ return new Promise((resolve, reject) => {
+ client.getSigningKey(header.kid, (err, key) => {
+ if (err) reject(err);
+ else resolve(key.getPublicKey());
+ });
+ });
+ }
+
+ async function checkPermissions(req, res, next) {
+ const authHeader = req.headers.authorization;
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
+ return res.status(401).send('Unauthorized: Missing token');
+ }
+ const token = authHeader.split(' ')[1];
+
+ try {
+ // Decode to get the header with kid
+ const decoded = jwt.decode(token, { complete: true });
+ const publicKey = await getPublicKey(decoded.header);
+
+ // Verify the token signature and claims
+ const verified = jwt.verify(token, publicKey, {
+ algorithms: ['RS256'],
+ complete: true
+ });
+
+ const decodedToken = verified.payload;
+
+ // Check if the API client app has the required scope
+ const requiredScope = 'deploy:applications';
+ if (decodedToken.scopes && decodedToken.scopes.includes(requiredScope)) {
+ // API client app has the required scope, proceed with the request
+ next();
+ } else {
+ // API client app does not have the required scope
+ res.status(403).send('Forbidden: Insufficient permissions');
+ }
+ } catch (error) {
+ // Token is invalid or expired
+ res.status(401).send('Unauthorized: Invalid token');
+ }
+ }
+ ```
+
+
+
+
+ ```python title="Example Flask decorator for scope validation" collapse={1-14}
+ # Security: ALWAYS validate the access token on your server before trusting its claims.
+ # This prevents token forgery and ensures the token has not expired.
+ import os
+ import functools
+ from scalekit import ScalekitClient
+ from flask import request, jsonify
+
+ # Initialize Scalekit client
+ scalekit_client = ScalekitClient(
+ env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"),
+ client_id=os.getenv("SCALEKIT_CLIENT_ID"),
+ client_secret=os.getenv("SCALEKIT_CLIENT_SECRET")
+ )
+
+ def check_permissions(required_scope):
+ def decorator(f):
+ @functools.wraps(f)
+ def decorated_function(*args, **kwargs):
+ auth_header = request.headers.get('Authorization')
+ if not auth_header or not auth_header.startswith('Bearer '):
+ return jsonify({"error": "Unauthorized: Missing token"}), 401
+
+ token = auth_header.split(' ')[1]
+
+ try:
+ # Validate the token using the Scalekit SDK
+ claims = scalekit_client.validate_access_token_and_get_claims(token=token)
+
+ # Check if the API client app has the required scope
+ if required_scope in claims.get('scopes', []):
+ # API client app has the required scope
+ return f(*args, **kwargs)
+ else:
+ # API client app does not have the required scope
+ return jsonify({"error": "Forbidden: Insufficient permissions"}), 403
+ except Exception as e:
+ # Token is invalid or expired
+ return jsonify({"error": "Unauthorized: Invalid token"}), 401
+ return decorated_function
+ return decorator
+
+ # Example usage in a Flask route
+ # @app.route('/deploy', methods=['POST'])
+ # @check_permissions('deploy:applications')
+ # def deploy_application():
+ # return jsonify({"message": "Deployment successful"})
+ ```
+
+
diff --git a/src/content/docs/authenticate/m2m/api-keys.mdx b/src/content/docs/authenticate/m2m/api-keys.mdx
new file mode 100644
index 000000000..84a6fb3c8
--- /dev/null
+++ b/src/content/docs/authenticate/m2m/api-keys.mdx
@@ -0,0 +1,988 @@
+---
+title: API keys
+description: "Issue long-lived, revocable API keys scoped to organizations and users for programmatic access to your APIs"
+sidebar:
+ label: "Add API keys"
+tags: [api-tokens, api-keys, m2m, authentication, bearer-tokens]
+head:
+ - tag: style
+ content: |
+ .sl-markdown-content h2 {
+ font-size: var(--sl-text-xl);
+ }
+ .sl-markdown-content h3 {
+ font-size: var(--sl-text-lg);
+ }
+---
+
+import { Aside, Steps, Tabs, TabItem, Card, CardGrid, LinkCard } from '@astrojs/starlight/components';
+import InstallSDK from '@components/templates/_installsdk.mdx';
+
+When your customers integrate with your APIs — whether for CI/CD pipelines, partner integrations, or internal tooling — they need a straightforward way to authenticate. Scalekit API keys give you long-lived, revocable bearer credentials for organization-level or user-level access to your APIs.
+
+In this guide, you'll learn how to create, validate, list, and revoke API keys using the Scalekit.
+
+```d2 pad=36
+shape: sequence_diagram
+
+Customer's App (API Client)
+User
+Your App
+Scalekit
+
+User -> Your App: Request API key
+Your App -> Scalekit: Create token (organizationId, userId?)
+Scalekit -> Your App: API key + tokenId
+Your App -> User: API key
+User -> Customer's App (API Client): Configure API key
+Customer's App (API Client) -> Your App: Request with Authorization header
+Your App -> Scalekit: Validate token
+Scalekit -> Your App: Organization ID (+ User ID if user-scoped)
+Your App -> Your App: Filter data by org and user context
+Your App -> Customer's App (API Client): Response
+```
+
+
+
+
+
+1. ## Install the SDK
+
+
+
+ Initialize the Scalekit client with your environment credentials:
+
+
+
+
+ ```javascript title="Express.js" collapse={1-2}
+ import { ScalekitClient } from '@scalekit-sdk/node';
+
+ const scalekit = new ScalekitClient(
+ process.env.SCALEKIT_ENVIRONMENT_URL,
+ process.env.SCALEKIT_CLIENT_ID,
+ process.env.SCALEKIT_CLIENT_SECRET
+ );
+ ```
+
+
+
+
+ ```python title="Flask" collapse={1-2}
+ import os
+ from scalekit import ScalekitClient
+
+ scalekit_client = ScalekitClient(
+ env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"],
+ client_id=os.environ["SCALEKIT_CLIENT_ID"],
+ client_secret=os.environ["SCALEKIT_CLIENT_SECRET"],
+ )
+ ```
+
+
+
+
+ ```go title="Gin" collapse={1-2}
+ import scalekit "github.com/scalekit-inc/scalekit-sdk-go/v2"
+
+ scalekitClient := scalekit.NewScalekitClient(
+ os.Getenv("SCALEKIT_ENVIRONMENT_URL"),
+ os.Getenv("SCALEKIT_CLIENT_ID"),
+ os.Getenv("SCALEKIT_CLIENT_SECRET"),
+ )
+ ```
+
+
+
+
+ ```java title="Spring Boot" collapse={1-2}
+ import com.scalekit.ScalekitClient;
+
+ ScalekitClient scalekitClient = new ScalekitClient(
+ System.getenv("SCALEKIT_ENVIRONMENT_URL"),
+ System.getenv("SCALEKIT_CLIENT_ID"),
+ System.getenv("SCALEKIT_CLIENT_SECRET")
+ );
+ ```
+
+
+
+
+2. ## Create a token
+
+ To get started, create an API key scoped to an organization. You can optionally scope it to a specific user and attach custom metadata.
+
+ ### Organization-scoped API key
+
+ **When to use**: Organization-scoped keys are for customers who need full access to all resources within their workspace or account. When they authenticate with the key, Scalekit validates it and confirms the organization context — your API then exposes all resources they own.
+
+ **Example scenario**: You're building a CRM like HubSpot. Your customer integrates with your API using an organization-scoped key. When they request contacts, tasks, or deals, the key validates successfully for their organization, and your API returns all resources in that workspace.
+
+ This is the most common pattern for service-to-service integrations where the API key represents access on behalf of an entire organization.
+
+
+
+
+ ```javascript
+ try {
+ const response = await scalekit.token.createToken(organizationId, {
+ description: 'CI/CD pipeline token',
+ });
+
+ // Store securely — this value cannot be retrieved again after creation
+ const opaqueToken = response.token;
+ // Stable identifier for management operations (format: apit_xxxxx)
+ const tokenId = response.tokenId;
+ } catch (error) {
+ console.error('Failed to create token:', error.message);
+ }
+ ```
+
+
+
+
+ ```python
+ try:
+ response = scalekit_client.tokens.create_token(
+ organization_id=organization_id,
+ description="CI/CD pipeline token",
+ )
+
+ opaque_token = response.token # store this securely
+ token_id = response.token_id # format: apit_xxxxx
+ except Exception as e:
+ print(f"Failed to create token: {e}")
+ ```
+
+
+
+
+ ```go
+ response, err := scalekitClient.Token().CreateToken(
+ ctx, organizationId, scalekit.CreateTokenOptions{
+ Description: "CI/CD pipeline token",
+ },
+ )
+ if err != nil {
+ log.Printf("Failed to create token: %v", err)
+ return
+ }
+
+ // Store securely — this value cannot be retrieved again after creation
+ opaqueToken := response.Token
+ // Stable identifier for management operations (format: apit_xxxxx)
+ tokenId := response.TokenId
+ ```
+
+
+
+
+ ```java
+ import com.scalekit.grpc.scalekit.v1.tokens.CreateTokenResponse;
+
+ try {
+ CreateTokenResponse response = scalekitClient.tokens().create(organizationId);
+
+ // Store securely — this value cannot be retrieved again after creation
+ String opaqueToken = response.getToken();
+ // Stable identifier for management operations (format: apit_xxxxx)
+ String tokenId = response.getTokenId();
+ } catch (Exception e) {
+ System.err.println("Failed to create token: " + e.getMessage());
+ }
+ ```
+
+
+
+
+ ### User-scoped API key
+
+ **When to use**: User-scoped keys enable fine-grained data filtering based on who owns the key. Your API validates the key, receives the user context, and then exposes only data relevant to that user — enabling role-based filtering without additional database lookups.
+
+ **Example scenario**: Your CRM has a `/tasks` endpoint. One customer gives their team member a user-scoped API key. When that person calls `/tasks`, the key validates for their organization _and_ user, and your API returns only tasks assigned to them — not all tasks in the workspace. Another team member with a different key sees only their own tasks.
+
+ User-scoped keys enable personal access tokens, per-user audit trails, and user-level rate limiting. You can also attach custom claims as key-value metadata.
+
+
+
+
+ ```javascript
+ try {
+ const userToken = await scalekit.token.createToken(organizationId, {
+ userId: 'usr_12345',
+ customClaims: {
+ team: 'engineering',
+ environment: 'production',
+ },
+ description: 'Deployment service token',
+ });
+
+ const opaqueToken = userToken.token;
+ const tokenId = userToken.tokenId;
+ } catch (error) {
+ console.error('Failed to create token:', error.message);
+ }
+ ```
+
+
+
+
+ ```python
+ try:
+ user_token = scalekit_client.tokens.create_token(
+ organization_id=organization_id,
+ user_id="usr_12345",
+ custom_claims={
+ "team": "engineering",
+ "environment": "production",
+ },
+ description="Deployment service token",
+ )
+
+ opaque_token = user_token.token
+ token_id = user_token.token_id
+ except Exception as e:
+ print(f"Failed to create token: {e}")
+ ```
+
+
+
+
+ ```go
+ userToken, err := scalekitClient.Token().CreateToken(
+ ctx, organizationId, scalekit.CreateTokenOptions{
+ UserId: "usr_12345",
+ CustomClaims: map[string]string{
+ "team": "engineering",
+ "environment": "production",
+ },
+ Description: "Deployment service token",
+ },
+ )
+ if err != nil {
+ log.Printf("Failed to create user token: %v", err)
+ return
+ }
+
+ opaqueToken := userToken.Token
+ tokenId := userToken.TokenId
+ ```
+
+
+
+
+ ```java
+ import java.util.Map;
+ import com.scalekit.grpc.scalekit.v1.tokens.CreateTokenResponse;
+
+ try {
+ Map customClaims = Map.of(
+ "team", "engineering",
+ "environment", "production"
+ );
+
+ CreateTokenResponse userToken = scalekitClient.tokens().create(
+ organizationId, "usr_12345", customClaims, null, "Deployment service token"
+ );
+
+ String opaqueToken = userToken.getToken();
+ String tokenId = userToken.getTokenId();
+ } catch (Exception e) {
+ System.err.println("Failed to create token: " + e.getMessage());
+ }
+ ```
+
+
+
+
+ The response contains three fields:
+
+ | Field | Description |
+ |-------|-------------|
+ | `token` | The API key string. **Returned only at creation.** |
+ | `token_id` | An identifier (format: `apit_xxxxx`) for referencing the token in management operations. |
+ | `token_info` | Metadata including organization, user, custom claims, and timestamps. |
+
+3. ## Validate a token
+
+ When your API server receives a request with an API key, you'll want to verify it's legitimate before processing the request. Pass the key to Scalekit — it validates the key server-side and returns the associated organization, user, and metadata context.
+
+
+
+
+ ```javascript
+ import { ScalekitValidateTokenFailureException } from '@scalekit-sdk/node';
+
+ try {
+ const result = await scalekit.token.validateToken(opaqueToken);
+
+ const orgId = result.tokenInfo?.organizationId;
+ const userId = result.tokenInfo?.userId;
+ const claims = result.tokenInfo?.customClaims;
+ } catch (error) {
+ if (error instanceof ScalekitValidateTokenFailureException) {
+ // Token is invalid, expired, or revoked
+ console.error('Token validation failed:', error.message);
+ }
+ }
+ ```
+
+
+
+
+ ```python
+ from scalekit import ScalekitValidateTokenFailureException
+
+ try:
+ result = scalekit_client.tokens.validate_token(token=opaque_token)
+
+ org_id = result.token_info.organization_id
+ user_id = result.token_info.user_id
+ claims = result.token_info.custom_claims
+ except ScalekitValidateTokenFailureException:
+ # Token is invalid, expired, or revoked
+ print("Token validation failed")
+ ```
+
+
+
+
+ ```go
+ result, err := scalekitClient.Token().ValidateToken(ctx, opaqueToken)
+ if errors.Is(err, scalekit.ErrTokenValidationFailed) {
+ // Token is invalid, expired, or revoked
+ log.Printf("Token validation failed: %v", err)
+ return
+ }
+
+ orgId := result.TokenInfo.OrganizationId
+ userId := result.TokenInfo.GetUserId() // *string — nil for org-scoped tokens
+ claims := result.TokenInfo.CustomClaims
+ ```
+
+
+
+
+ ```java
+ import java.util.Map;
+ import com.scalekit.exceptions.TokenInvalidException;
+ import com.scalekit.grpc.scalekit.v1.tokens.ValidateTokenResponse;
+
+ try {
+ ValidateTokenResponse result = scalekitClient.tokens().validate(opaqueToken);
+
+ String orgId = result.getTokenInfo().getOrganizationId();
+ String userId = result.getTokenInfo().getUserId();
+ Map claims = result.getTokenInfo().getCustomClaimsMap();
+ } catch (TokenInvalidException e) {
+ // Token is invalid, expired, or revoked
+ System.err.println("Token validation failed: " + e.getMessage());
+ }
+ ```
+
+
+
+
+ If the API key is invalid, expired, or has been revoked, validation fails with a specific error that you can catch and handle in your code. This makes it easy to reject unauthorized requests in your API middleware.
+
+ ### Access roles and organization details
+
+ Beyond the basic organization and user information, the validation response also includes any roles assigned to the user and external identifiers you've configured for the organization. These are useful for making authorization decisions without additional database lookups.
+
+
+
+
+ ```javascript
+ try {
+ const result = await scalekit.token.validateToken(opaqueToken);
+
+ // Roles assigned to the user
+ const roles = result.tokenInfo?.roles;
+
+ // External identifiers for mapping to your system
+ const externalOrgId = result.tokenInfo?.organizationExternalId;
+ const externalUserId = result.tokenInfo?.userExternalId;
+ } catch (error) {
+ if (error instanceof ScalekitValidateTokenFailureException) {
+ console.error('Token validation failed:', error.message);
+ }
+ }
+ ```
+
+
+
+
+ ```python
+ try:
+ result = scalekit_client.tokens.validate_token(token=opaque_token)
+
+ # Roles assigned to the user
+ roles = result.token_info.roles
+
+ # External identifiers for mapping to your system
+ external_org_id = result.token_info.organization_external_id
+ external_user_id = result.token_info.user_external_id
+ except ScalekitValidateTokenFailureException:
+ print("Token validation failed")
+ ```
+
+
+
+
+ ```go
+ result, err := scalekitClient.Token().ValidateToken(ctx, opaqueToken)
+ if errors.Is(err, scalekit.ErrTokenValidationFailed) {
+ log.Printf("Token validation failed: %v", err)
+ return
+ }
+
+ // Roles assigned to the user
+ roles := result.TokenInfo.Roles
+
+ // External identifiers for mapping to your system
+ externalOrgId := result.TokenInfo.OrganizationExternalId
+ externalUserId := result.TokenInfo.GetUserExternalId() // *string — nil if no external ID
+ ```
+
+
+
+
+ ```java
+ import java.util.List;
+ import com.scalekit.exceptions.TokenInvalidException;
+ import com.scalekit.grpc.scalekit.v1.tokens.ValidateTokenResponse;
+
+ try {
+ ValidateTokenResponse result = scalekitClient.tokens().validate(opaqueToken);
+
+ // Roles assigned to the user
+ List roles = result.getTokenInfo().getRolesList();
+
+ // External identifiers for mapping to your system
+ String externalOrgId = result.getTokenInfo().getOrganizationExternalId();
+ String externalUserId = result.getTokenInfo().getUserExternalId();
+ } catch (TokenInvalidException e) {
+ System.err.println("Token validation failed: " + e.getMessage());
+ }
+ ```
+
+
+
+
+
+
+ ### Access custom metadata
+
+ If you attached custom claims when creating the API key, they come back in every validation response. This is a convenient way to make fine-grained authorization decisions — like restricting access by team or environment — without hitting your database.
+
+
+
+
+ ```javascript
+ try {
+ const result = await scalekit.token.validateToken(opaqueToken);
+
+ const team = result.tokenInfo?.customClaims?.team;
+ const environment = result.tokenInfo?.customClaims?.environment;
+
+ // Use metadata for authorization
+ if (environment !== 'production') {
+ return res.status(403).json({ error: 'Production access required' });
+ }
+ } catch (error) {
+ if (error instanceof ScalekitValidateTokenFailureException) {
+ console.error('Token validation failed:', error.message);
+ }
+ }
+ ```
+
+
+
+
+ ```python
+ try:
+ result = scalekit_client.tokens.validate_token(token=opaque_token)
+
+ team = result.token_info.custom_claims.get("team")
+ environment = result.token_info.custom_claims.get("environment")
+
+ # Use metadata for authorization
+ if environment != "production":
+ return jsonify({"error": "Production access required"}), 403
+ except ScalekitValidateTokenFailureException:
+ print("Token validation failed")
+ ```
+
+
+
+
+ ```go
+ result, err := scalekitClient.Token().ValidateToken(ctx, opaqueToken)
+ if errors.Is(err, scalekit.ErrTokenValidationFailed) {
+ log.Printf("Token validation failed: %v", err)
+ return
+ }
+
+ team := result.TokenInfo.CustomClaims["team"]
+ environment := result.TokenInfo.CustomClaims["environment"]
+
+ // Use metadata for authorization
+ if environment != "production" {
+ c.JSON(403, gin.H{"error": "Production access required"})
+ return
+ }
+ ```
+
+
+
+
+ ```java
+ import java.util.Map;
+ import com.scalekit.exceptions.TokenInvalidException;
+ import com.scalekit.grpc.scalekit.v1.tokens.ValidateTokenResponse;
+
+ try {
+ ValidateTokenResponse result = scalekitClient.tokens().validate(opaqueToken);
+
+ String team = result.getTokenInfo().getCustomClaimsMap().get("team");
+ String environment = result.getTokenInfo().getCustomClaimsMap().get("environment");
+
+ // Use metadata for authorization
+ if (!"production".equals(environment)) {
+ return ResponseEntity.status(403).body(Map.of("error", "Production access required"));
+ }
+ } catch (TokenInvalidException e) {
+ System.err.println("Token validation failed: " + e.getMessage());
+ }
+ ```
+
+
+
+
+4. ## List tokens
+
+ You can retrieve all active API keys for an organization at any time. The response supports pagination for large result sets, and you can filter by user to find keys scoped to a specific person.
+
+
+
+
+ ```javascript
+ try {
+ // List tokens for an organization
+ const response = await scalekit.token.listTokens(organizationId, {
+ pageSize: 10,
+ });
+
+ for (const token of response.tokens) {
+ console.log(token.tokenId, token.description);
+ }
+
+ // Paginate through results
+ if (response.nextPageToken) {
+ const nextPage = await scalekit.token.listTokens(organizationId, {
+ pageSize: 10,
+ pageToken: response.nextPageToken,
+ });
+ }
+
+ // Filter tokens by user
+ const userTokens = await scalekit.token.listTokens(organizationId, {
+ userId: 'usr_12345',
+ });
+ } catch (error) {
+ console.error('Failed to list tokens:', error.message);
+ }
+ ```
+
+
+
+
+ ```python
+ try:
+ # List tokens for an organization
+ response = scalekit_client.tokens.list_tokens(
+ organization_id=organization_id,
+ page_size=10,
+ )
+
+ for token in response.tokens:
+ print(token.token_id, token.description)
+
+ # Paginate through results
+ if response.next_page_token:
+ next_page = scalekit_client.tokens.list_tokens(
+ organization_id=organization_id,
+ page_size=10,
+ page_token=response.next_page_token,
+ )
+
+ # Filter tokens by user
+ user_tokens = scalekit_client.tokens.list_tokens(
+ organization_id=organization_id,
+ user_id="usr_12345",
+ )
+ except Exception as e:
+ print(f"Failed to list tokens: {e}")
+ ```
+
+
+
+
+ ```go
+ // List tokens for an organization
+ response, err := scalekitClient.Token().ListTokens(
+ ctx, organizationId, scalekit.ListTokensOptions{
+ PageSize: 10,
+ },
+ )
+ if err != nil {
+ log.Printf("Failed to list tokens: %v", err)
+ return
+ }
+
+ for _, token := range response.Tokens {
+ fmt.Println(token.TokenId, token.GetDescription())
+ }
+
+ // Paginate through results
+ if response.NextPageToken != "" {
+ nextPage, err := scalekitClient.Token().ListTokens(
+ ctx, organizationId, scalekit.ListTokensOptions{
+ PageSize: 10,
+ PageToken: response.NextPageToken,
+ },
+ )
+ if err != nil {
+ log.Printf("Failed to fetch next page: %v", err)
+ return
+ }
+ _ = nextPage // process nextPage.Tokens
+ }
+
+ // Filter tokens by user
+ userTokens, err := scalekitClient.Token().ListTokens(
+ ctx, organizationId, scalekit.ListTokensOptions{
+ UserId: "usr_12345",
+ },
+ )
+ if err != nil {
+ log.Printf("Failed to list user tokens: %v", err)
+ return
+ }
+ _ = userTokens // process userTokens.Tokens
+ ```
+
+
+
+
+ ```java
+ import com.scalekit.grpc.scalekit.v1.tokens.ListTokensResponse;
+ import com.scalekit.grpc.scalekit.v1.tokens.Token;
+
+ try {
+ ListTokensResponse response = scalekitClient.tokens().list(organizationId, 10, null);
+ for (Token token : response.getTokensList()) {
+ System.out.println(token.getTokenId() + " " + token.getDescription());
+ }
+ } catch (Exception e) {
+ System.err.println("Failed to list tokens: " + e.getMessage());
+ }
+
+ try {
+ ListTokensResponse response = scalekitClient.tokens().list(organizationId, 10, null);
+ if (!response.getNextPageToken().isEmpty()) {
+ ListTokensResponse nextPage = scalekitClient.tokens().list(
+ organizationId, 10, response.getNextPageToken()
+ );
+ }
+ } catch (Exception e) {
+ System.err.println("Failed to paginate tokens: " + e.getMessage());
+ }
+
+ try {
+ ListTokensResponse userTokens = scalekitClient.tokens().list(
+ organizationId, "usr_12345", 10, null
+ );
+ } catch (Exception e) {
+ System.err.println("Failed to list user tokens: " + e.getMessage());
+ }
+ ```
+
+
+
+
+ The response includes `totalCount` for the total number of matching tokens and `nextPageToken` / `prevPageToken` cursors for navigating pages.
+
+5. ## Invalidate a token
+
+ When you need to revoke an API key — for example, when an employee leaves or you suspect credentials have been compromised — you can invalidate it through Scalekit. Revocation takes effect instantly: the very next validation request for that key will fail.
+
+ This operation is **idempotent**, so calling invalidate on an already-revoked key succeeds without error.
+
+
+
+
+ ```javascript
+ try {
+ // Invalidate by API key string
+ await scalekit.token.invalidateToken(opaqueToken);
+
+ // Or invalidate by token_id (useful when you store tokenId for lifecycle management)
+ await scalekit.token.invalidateToken(tokenId);
+ } catch (error) {
+ console.error('Failed to invalidate token:', error.message);
+ }
+ ```
+
+
+
+
+ ```python
+ try:
+ # Invalidate by API key string
+ scalekit_client.tokens.invalidate_token(token=opaque_token)
+
+ # Or invalidate by token_id (useful when you store token_id for lifecycle management)
+ scalekit_client.tokens.invalidate_token(token=token_id)
+ except Exception as e:
+ print(f"Failed to invalidate token: {e}")
+ ```
+
+
+
+
+ ```go
+ // Invalidate by API key string
+ if err := scalekitClient.Token().InvalidateToken(ctx, opaqueToken); err != nil {
+ log.Printf("Failed to invalidate token: %v", err)
+ }
+
+ // Or invalidate by token_id (useful when you store tokenId for lifecycle management)
+ if err := scalekitClient.Token().InvalidateToken(ctx, tokenId); err != nil {
+ log.Printf("Failed to invalidate token: %v", err)
+ }
+ ```
+
+
+
+
+ ```java
+ try {
+ // Invalidate by API key string
+ scalekitClient.tokens().invalidate(opaqueToken);
+
+ // Or invalidate by token_id (useful when you store tokenId for lifecycle management)
+ scalekitClient.tokens().invalidate(tokenId);
+ } catch (Exception e) {
+ System.err.println("Failed to invalidate token: " + e.getMessage());
+ }
+ ```
+
+
+
+
+6. ## Protect your API endpoints
+
+ Now let's put it all together. The most common pattern is to add API key validation as middleware in your API server. Extract the Bearer token from the `Authorization` header, validate it through Scalekit, and use the returned context for authorization decisions.
+
+
+
+
+ ```javascript title="Express.js"
+ import { ScalekitValidateTokenFailureException } from '@scalekit-sdk/node';
+
+ async function authenticateToken(req, res, next) {
+ const authHeader = req.headers['authorization'];
+ const token = authHeader && authHeader.split(' ')[1];
+
+ if (!token) {
+ // Reject requests without credentials to prevent unauthorized access
+ return res.status(401).json({ error: 'Missing authorization token' });
+ }
+
+ try {
+ // Server-side validation — Scalekit checks token status in real time
+ const result = await scalekit.token.validateToken(token);
+ // Attach token context to the request for downstream handlers
+ req.tokenInfo = result.tokenInfo;
+ next();
+ } catch (error) {
+ if (error instanceof ScalekitValidateTokenFailureException) {
+ // Revoked, expired, or malformed tokens are rejected immediately
+ return res.status(401).json({ error: 'Invalid or expired token' });
+ }
+ throw error;
+ }
+ }
+
+ // Apply to protected routes
+ app.get('/api/resources', authenticateToken, (req, res) => {
+ const orgId = req.tokenInfo.organizationId;
+ // Serve resources scoped to this organization
+ });
+ ```
+
+
+
+
+ ```python title="Flask"
+ from functools import wraps
+ from flask import request, jsonify, g
+ from scalekit import ScalekitValidateTokenFailureException
+
+ def authenticate_token(f):
+ @wraps(f)
+ def decorated(*args, **kwargs):
+ auth_header = request.headers.get("Authorization", "")
+ if not auth_header.startswith("Bearer "):
+ # Reject requests without credentials to prevent unauthorized access
+ return jsonify({"error": "Missing authorization token"}), 401
+
+ token = auth_header.split(" ")[1]
+
+ try:
+ # Server-side validation — Scalekit checks token status in real time
+ result = scalekit_client.tokens.validate_token(token=token)
+ # Attach token context for downstream handlers
+ g.token_info = result.token_info
+ except ScalekitValidateTokenFailureException:
+ # Revoked, expired, or malformed tokens are rejected immediately
+ return jsonify({"error": "Invalid or expired token"}), 401
+
+ return f(*args, **kwargs)
+ return decorated
+
+ # Apply to protected routes
+ @app.route("/api/resources")
+ @authenticate_token
+ def get_resources():
+ org_id = g.token_info.organization_id
+ # Serve resources scoped to this organization
+ ```
+
+
+
+
+ ```go title="Gin"
+ func AuthenticateToken(scalekitClient scalekit.Scalekit) gin.HandlerFunc {
+ return func(c *gin.Context) {
+ authHeader := c.GetHeader("Authorization")
+ if !strings.HasPrefix(authHeader, "Bearer ") {
+ // Reject requests without credentials to prevent unauthorized access
+ c.JSON(401, gin.H{"error": "Missing authorization token"})
+ c.Abort()
+ return
+ }
+
+ token := strings.TrimPrefix(authHeader, "Bearer ")
+
+ // Server-side validation — Scalekit checks token status in real time
+ result, err := scalekitClient.Token().ValidateToken(c.Request.Context(), token)
+ if err != nil {
+ if errors.Is(err, scalekit.ErrTokenValidationFailed) {
+ // Revoked, expired, or malformed tokens are rejected immediately
+ c.JSON(401, gin.H{"error": "Invalid or expired token"})
+ } else {
+ // Surface transport or unexpected errors as 500
+ c.JSON(500, gin.H{"error": "Internal server error"})
+ }
+ c.Abort()
+ return
+ }
+
+ // Attach token context for downstream handlers
+ c.Set("tokenInfo", result.TokenInfo)
+ c.Next()
+ }
+ }
+
+ // Apply to protected routes
+ r.GET("/api/resources", AuthenticateToken(scalekitClient), func(c *gin.Context) {
+ tokenInfo := c.MustGet("tokenInfo").(*scalekit.TokenInfo)
+ orgId := tokenInfo.OrganizationId
+ // Serve resources scoped to this organization
+ })
+ ```
+
+
+
+
+ ```java title="Spring Boot"
+ import com.scalekit.exceptions.TokenInvalidException;
+ import com.scalekit.grpc.scalekit.v1.tokens.Token;
+ import com.scalekit.grpc.scalekit.v1.tokens.ValidateTokenResponse;
+
+ @Component
+ public class TokenAuthFilter extends OncePerRequestFilter {
+ private final ScalekitClient scalekitClient;
+
+ public TokenAuthFilter(ScalekitClient scalekitClient) {
+ this.scalekitClient = scalekitClient;
+ }
+
+ @Override
+ protected void doFilterInternal(
+ HttpServletRequest request,
+ HttpServletResponse response,
+ FilterChain filterChain
+ ) throws ServletException, IOException {
+ String authHeader = request.getHeader("Authorization");
+ if (authHeader == null || !authHeader.startsWith("Bearer ")) {
+ // Reject requests without credentials to prevent unauthorized access
+ response.sendError(401, "Missing authorization token");
+ return;
+ }
+
+ String token = authHeader.substring(7);
+
+ try {
+ // Server-side validation — Scalekit checks token status in real time
+ ValidateTokenResponse result = scalekitClient.tokens().validate(token);
+ // Attach token context for downstream handlers
+ request.setAttribute("tokenInfo", result.getTokenInfo());
+ filterChain.doFilter(request, response);
+ } catch (TokenInvalidException e) {
+ // Revoked, expired, or malformed tokens are rejected immediately
+ response.sendError(401, "Invalid or expired token");
+ }
+ }
+ }
+
+ // Access in your controller
+ @GetMapping("/api/resources")
+ public ResponseEntity> getResources(HttpServletRequest request) {
+ Token tokenInfo = (Token) request.getAttribute("tokenInfo");
+ String orgId = tokenInfo.getOrganizationId();
+ // Serve resources scoped to this organization
+ }
+ ```
+
+
+
+
+ ### Using validation context for data filtering
+
+ After validation succeeds, your middleware has access to the organization and (optionally) user context. Use this context to filter the data your endpoint returns — no additional database queries needed.
+
+ **For organization-scoped keys**: Extract the organization ID from the validation response. Your endpoint then returns resources belonging to that organization. If a customer authenticates with an organization-scoped key, they get access to all their workspace data.
+
+ **For user-scoped keys**: Extract both organization ID and user ID. Filter your query to return only resources belonging to that user within the organization. If a team member authenticates with a user-scoped key, they see only their assigned tasks, their owned projects, or their allocated resources — depending on your application logic.
+
+ The validation response is your source of truth. Trust the organization and user context it provides, and use it to build your authorization queries without additional lookups.
+
+ Here are a few tips to help you get the most out of API keys in production:
+
+ - **Store API keys securely**: Treat API keys like passwords. Store them in encrypted secrets managers or environment variables. Never log keys, commit them to version control, or expose them in client-side code.
+ - **Set expiry for time-limited access**: Use the `expiry` parameter for keys that should automatically become invalid after a set period. This limits the blast radius if a key is compromised.
+ - **Use custom claims for context**: Attach metadata like `team`, `environment`, or `service` as custom claims. Your API middleware can use these claims for fine-grained authorization without additional database lookups.
+ - **Rotate keys safely**: To rotate an API key, create a new key, update the consuming service to use the new key, verify the new key works, then invalidate the old key. This avoids downtime during rotation.
+
+ You now have everything you need to issue, validate, and manage API keys in your application.
+
+
\ No newline at end of file
diff --git a/src/content/docs/guides/m2m/api-auth-m2m-clients.mdx b/src/content/docs/guides/m2m/api-auth-m2m-clients.mdx
index 365d6cee8..c32263bf0 100644
--- a/src/content/docs/guides/m2m/api-auth-m2m-clients.mdx
+++ b/src/content/docs/guides/m2m/api-auth-m2m-clients.mdx
@@ -8,12 +8,6 @@ browseCentral:
icon: "book"
sidebar:
label: Authenticate customer apps
-prev:
- link: '/guides/m2m/scopes/'
- label: 'Define & validate scopes'
-next:
- link: '/authenticate/mcp/intro-to-mcp-auth/'
- label: 'Understanding MCP authentication'
seeAlso:
label: "Code samples"
expanded: true
diff --git a/src/content/docs/guides/m2m/scopes.mdx b/src/content/docs/guides/m2m/scopes.mdx
deleted file mode 100644
index db266ef92..000000000
--- a/src/content/docs/guides/m2m/scopes.mdx
+++ /dev/null
@@ -1,546 +0,0 @@
----
-title: Verify API client scopes
-description: Define permissions for API client applications using scopes and validate them in your API server
-browseCentral:
- label: "Define scopes and verify server-side"
- filterType: ["tutorial"]
- category: ["API authentication"]
- icon: "book"
-sidebar:
- label: Verify API client scopes
----
-
-import { Aside, Steps, Tabs, TabItem } from '@astrojs/starlight/components';
-
-Scopes define which permissions an API client application can request when accessing your APIs. When you register an API client for your customers, you assign scopes that determine what actions the client app can perform. Scalekit includes these scopes in the access token when the client app authenticates.
-
-This guide shows you how to register API clients with specific scopes for your customers and how to validate those scopes in your API server.
-
-
-1. ## Register an API client with scopes
-
- When you enable API client registration for your customers, you define the scopes that their applications can request. For example, you might create an API client for a customer's deployment service with scopes like `deploy:applications` and `read:deployments`.
-
-
-
-
- ```javascript title="Register an API client with specific scopes"
- // Use case: Your customer requests API access for their deployment automation.
- // You register an API client app with the appropriate scopes.
- import { ScalekitClient } from '@scalekit-sdk/node';
-
- // Initialize Scalekit client (see installation guide for setup)
- const scalekit = new ScalekitClient(
- process.env.SCALEKIT_ENVIRONMENT_URL,
- process.env.SCALEKIT_CLIENT_ID,
- process.env.SCALEKIT_CLIENT_SECRET
- );
-
- async function createAPIClient() {
- try {
- // Define API client details with scopes your customer's app needs
- const clientDetails = {
- name: 'GitHub Actions Deployment Service',
- description: 'Service account for GitHub Actions to deploy applications to production',
- scopes: ['deploy:applications', 'read:deployments'],
- expiry: 3600, // Token expiry in seconds
- };
-
- // API call to register the client
- const response = await scalekit.m2m.createClient({
- organizationId: process.env.SCALEKIT_ORGANIZATION_ID,
- client: clientDetails,
- });
-
- // Response contains client details and the plain_secret (only returned once)
- const clientId = response.client.client_id;
- const plainSecret = response.plain_secret;
-
- // Provide these credentials to your customer securely
- console.log('Created API client:', clientId);
- } catch (error) {
- console.error('Error creating API client:', error);
- }
- }
-
- createAPIClient();
- ```
-
-
-
-
- ```python title="Register an API client with specific scopes"
- # Use case: Your customer requests API access for their deployment automation.
- # You register an API client app with the appropriate scopes.
- import os
- from scalekit import ScalekitClient
-
- # Initialize Scalekit client (see installation guide for setup)
- scalekit_client = ScalekitClient(
- env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"),
- client_id=os.getenv("SCALEKIT_CLIENT_ID"),
- client_secret=os.getenv("SCALEKIT_CLIENT_SECRET")
- )
-
- try:
- # Define API client details with scopes your customer's app needs
- from scalekit.v1.clients.clients_pb2 import OrganizationClient
-
- client_details = OrganizationClient(
- name="GitHub Actions Deployment Service",
- description="Service account for GitHub Actions to deploy applications to production",
- scopes=["deploy:applications", "read:deployments"],
- expiry=3600 # Token expiry in seconds
- )
-
- # API call to register the client
- response = scalekit_client.m2m_client.create_organization_client(
- organization_id=os.getenv("SCALEKIT_ORGANIZATION_ID"),
- m2m_client=client_details
- )
-
- # Response contains client details and the plain_secret (only returned once)
- client_id = response.client.client_id
- plain_secret = response.plain_secret
-
- # Provide these credentials to your customer securely
- print("Created API client:", client_id)
-
- except Exception as e:
- print("Error creating API client:", e)
- ```
-
-
-
-
- ```go title="Register an API client with specific scopes"
- // Use case: Your customer requests API access for their deployment automation.
- // You register an API client app with the appropriate scopes.
- package main
-
- import (
- "context"
- "fmt"
- "os"
- "time"
-
- "github.com/scalekit-inc/scalekit-sdk-go"
- "github.com/scalekit-inc/scalekit-sdk-go/pkg/grpc/organizations"
- )
-
- func main() {
- // Initialize Scalekit client (see installation guide for setup)
- scalekitClient := scalekit.NewScalekitClient(
- os.Getenv("SCALEKIT_ENVIRONMENT_URL"),
- os.Getenv("SCALEKIT_CLIENT_ID"),
- os.Getenv("SCALEKIT_CLIENT_SECRET"),
- )
-
- ctx := context.Background()
-
- // Define API client details with scopes your customer's app needs
- clientDetails := &organizations.OrganizationClient{
- Name: "GitHub Actions Deployment Service",
- Description: "Service account for GitHub Actions to deploy applications to production",
- Scopes: []string{"deploy:applications", "read:deployments"},
- Expiry: int64(time.Hour.Seconds()), // Token expiry in seconds
- }
-
- // API call to register the client
- response, err := scalekitClient.Organization.CreateOrganizationClient(
- ctx,
- os.Getenv("SCALEKIT_ORGANIZATION_ID"),
- clientDetails,
- )
- if err != nil {
- fmt.Println("Error creating API client:", err)
- return
- }
-
- // Response contains client details and the plain_secret (only returned once)
- clientID := response.Client.ClientId
- plainSecret := response.PlainSecret
-
- // Provide these credentials to your customer securely
- fmt.Println("Created API client:", clientID)
- }
- ```
-
-
-
-
- ```java title="Register an API client with specific scopes"
- // Use case: Your customer requests API access for their deployment automation.
- // You register an API client app with the appropriate scopes.
- import com.scalekit.ScalekitClient;
- import com.scalekit.grpc.scalekit.v1.organizations.OrganizationClient;
- import com.scalekit.grpc.scalekit.v1.organizations.CreateOrganizationClientResponse;
- import java.util.Arrays;
-
- public class CreateAPIClient {
- public static void main(String[] args) {
- // Initialize Scalekit client (see installation guide for setup)
- ScalekitClient scalekitClient = new ScalekitClient(
- System.getenv("SCALEKIT_ENVIRONMENT_URL"),
- System.getenv("SCALEKIT_CLIENT_ID"),
- System.getenv("SCALEKIT_CLIENT_SECRET")
- );
-
- try {
- // Define API client details with scopes your customer's app needs
- OrganizationClient clientDetails = OrganizationClient.newBuilder()
- .setName("GitHub Actions Deployment Service")
- .setDescription("Service account for GitHub Actions to deploy applications to production")
- .addAllScopes(Arrays.asList("deploy:applications", "read:deployments"))
- .setExpiry(3600L) // Token expiry in seconds
- .build();
-
- // API call to register the client
- CreateOrganizationClientResponse response = scalekitClient
- .organizations()
- .createOrganizationClient(
- System.getenv("SCALEKIT_ORGANIZATION_ID"),
- clientDetails
- );
-
- // Response contains client details and the plain_secret (only returned once)
- String clientId = response.getClient().getClientId();
- String plainSecret = response.getPlainSecret();
-
- // Provide these credentials to your customer securely
- System.out.println("Created API client: " + clientId);
-
- } catch (Exception e) {
- System.err.println("Error creating API client: " + e.getMessage());
- }
- }
- }
- ```
-
-
-
-
- ```bash title="Register an API client with specific scopes" wrap
- curl -L 'https:///api/v1/organizations//clients' \
- -H 'Content-Type: application/json' \
- -H 'Authorization: Bearer ' \
- -d '{
- "name": "GitHub Actions Deployment Service",
- "description": "Service account for GitHub Actions to deploy applications to production",
- "scopes": [
- "deploy:applications",
- "read:deployments"
- ],
- "expiry": 3600
- }'
- ```
-
- ```json title="Sample response"
- {
- "client": {
- "client_id": "m2morg_68315758685323697",
- "secrets": [
- {
- "id": "sks_68315758802764209",
- "create_time": "2025-04-16T06:56:05.360Z",
- "update_time": "2025-04-16T06:56:05.367190455Z",
- "secret_suffix": "UZ0X",
- "status": "ACTIVE",
- "last_used_time": "2025-04-16T06:56:05.360Z"
- }
- ],
- "name": "GitHub Actions Deployment Service",
- "description": "Service account for GitHub Actions to deploy applications to production",
- "organization_id": "org_59615193906282635",
- "create_time": "2025-04-16T06:56:05.290Z",
- "update_time": "2025-04-16T06:56:05.292145150Z",
- "scopes": [
- "deploy:applications",
- "read:deployments"
- ]
- },
- "plain_secret": "test_ly8G57h0ErRJSObJI6dShkoaq6bigo11Dxcfa6reKG1kKNVbqBKW4H5Ctmb5UZ0X"
- }
- ```
-
-
-
-
-
-
-
-
-2. ## Validate API client scopes
-
- When your API server receives a request from an API client app, you must validate the scopes present in the access token provided in the `Authorization` header. The access token is a JSON Web Token (JWT).
-
- First, let's look at the claims inside a decoded JWT payload. Scalekit encodes the granted scopes in the `scopes` field.
-
- ```json title="Example decoded access token" showLineNumbers=false
- {
- "client_id": "m2morg_69038819013296423",
- "exp": 1745305340,
- "iat": 1745218940,
- "iss": "",
- "jti": "tkn_69041163914445100",
- "nbf": 1745218940,
- "oid": "org_59615193906282635",
- "scopes": [
- "deploy:applications",
- "read:deployments"
- ],
- "sub": "m2morg_69038819013296423"
- }
- ```
-
-
-
- Your API server should inspect the `scopes` array in the token payload to authorize the requested operation. Here's how you validate the token and check for a specific scope in your API server.
-
-
-
-
- ```javascript title="Example Express.js middleware for scope validation"
- // Security: ALWAYS validate the access token on your server before trusting its claims.
- // This prevents token forgery and ensures the token has not expired.
- import { ScalekitClient } from '@scalekit-sdk/node';
- import jwt from 'jsonwebtoken';
- import jwksClient from 'jwks-rsa';
-
- const scalekit = new ScalekitClient(
- process.env.SCALEKIT_ENVIRONMENT_URL,
- process.env.SCALEKIT_CLIENT_ID,
- process.env.SCALEKIT_CLIENT_SECRET
- );
-
- // Setup JWKS client for token verification
- const client = jwksClient({
- jwksUri: `${process.env.SCALEKIT_ENVIRONMENT_URL}/.well-known/jwks.json`,
- cache: true
- });
-
- async function getPublicKey(header) {
- return new Promise((resolve, reject) => {
- client.getSigningKey(header.kid, (err, key) => {
- if (err) reject(err);
- else resolve(key.getPublicKey());
- });
- });
- }
-
- async function checkPermissions(req, res, next) {
- const authHeader = req.headers.authorization;
- if (!authHeader || !authHeader.startsWith('Bearer ')) {
- return res.status(401).send('Unauthorized: Missing token');
- }
- const token = authHeader.split(' ')[1];
-
- try {
- // Decode to get the header with kid
- const decoded = jwt.decode(token, { complete: true });
- const publicKey = await getPublicKey(decoded.header);
-
- // Verify the token signature and claims
- const verified = jwt.verify(token, publicKey, {
- algorithms: ['RS256'],
- complete: true
- });
-
- const decodedToken = verified.payload;
-
- // Check if the API client app has the required scope
- const requiredScope = 'deploy:applications';
- if (decodedToken.scopes && decodedToken.scopes.includes(requiredScope)) {
- // API client app has the required scope, proceed with the request
- next();
- } else {
- // API client app does not have the required scope
- res.status(403).send('Forbidden: Insufficient permissions');
- }
- } catch (error) {
- // Token is invalid or expired
- res.status(401).send('Unauthorized: Invalid token');
- }
- }
- ```
-
-
-
-
- ```python title="Example Flask decorator for scope validation"
- # Security: ALWAYS validate the access token on your server before trusting its claims.
- # This prevents token forgery and ensures the token has not expired.
- import os
- import functools
- from scalekit import ScalekitClient
- from flask import request, jsonify
-
- # Initialize Scalekit client
- scalekit_client = ScalekitClient(
- env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"),
- client_id=os.getenv("SCALEKIT_CLIENT_ID"),
- client_secret=os.getenv("SCALEKIT_CLIENT_SECRET")
- )
-
- def check_permissions(required_scope):
- def decorator(f):
- @functools.wraps(f)
- def decorated_function(*args, **kwargs):
- auth_header = request.headers.get('Authorization')
- if not auth_header or not auth_header.startswith('Bearer '):
- return jsonify({"error": "Unauthorized: Missing token"}), 401
-
- token = auth_header.split(' ')[1]
-
- try:
- # Validate the token using the Scalekit SDK
- claims = scalekit_client.validate_access_token_and_get_claims(token=token)
-
- # Check if the API client app has the required scope
- if required_scope in claims.get('scopes', []):
- # API client app has the required scope
- return f(*args, **kwargs)
- else:
- # API client app does not have the required scope
- return jsonify({"error": "Forbidden: Insufficient permissions"}), 403
- except Exception as e:
- # Token is invalid or expired
- return jsonify({"error": "Unauthorized: Invalid token"}), 401
- return decorated_function
- return decorator
-
- # Example usage in a Flask route
- # @app.route('/deploy', methods=['POST'])
- # @check_permissions('deploy:applications')
- # def deploy_application():
- # return jsonify({"message": "Deployment successful"})
- ```
-
-
-
-
- ```go title="Example Go middleware for scope validation"
- // Security: ALWAYS validate the access token on your server before trusting its claims.
- // This prevents token forgery and ensures the token has not expired.
- package main
-
- import (
- "context"
- "net/http"
- "os"
- "strings"
-
- "github.com/scalekit-inc/scalekit-sdk-go"
- )
-
- func CheckPermissions(next http.Handler, requiredScope string) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- authHeader := r.Header.Get("Authorization")
- if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
- http.Error(w, "Unauthorized: Missing token", http.StatusUnauthorized)
- return
- }
- token := strings.TrimPrefix(authHeader, "Bearer ")
-
- // Initialize Scalekit client (should be a singleton in a real app)
- scalekitClient := scalekit.NewScalekitClient(
- os.Getenv("SCALEKIT_ENVIRONMENT_URL"),
- os.Getenv("SCALEKIT_CLIENT_ID"),
- os.Getenv("SCALEKIT_CLIENT_SECRET"),
- )
-
- // Validate the token using the Scalekit SDK
- decodedToken, err := scalekitClient.Token.VerifyToken(context.Background(), token, nil)
- if err != nil {
- http.Error(w, "Unauthorized: Invalid token", http.StatusUnauthorized)
- return
- }
-
- // Check if the API client app has the required scope
- hasScope := false
- for _, scope := range decodedToken.Claims.Scopes {
- if scope == requiredScope {
- hasScope = true
- break
- }
- }
-
- if hasScope {
- // API client app has the required scope
- next.ServeHTTP(w, r)
- } else {
- // API client app does not have the required scope
- http.Error(w, "Forbidden: Insufficient permissions", http.StatusForbidden)
- }
- })
- }
- ```
-
-
-
-
- ```java title="Example Spring Boot interceptor for scope validation"
- // Security: ALWAYS validate the access token on your server before trusting its claims.
- // This prevents token forgery and ensures the token has not expired.
- import com.scalekit.ScalekitClient;
- import com.scalekit.internal.http.TokenClaims;
- import org.springframework.stereotype.Component;
- import org.springframework.web.servlet.HandlerInterceptor;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
-
- @Component
- public class PermissionInterceptor implements HandlerInterceptor {
-
- private final ScalekitClient scalekitClient;
-
- public PermissionInterceptor(ScalekitClient scalekitClient) {
- this.scalekitClient = scalekitClient;
- }
-
- @Override
- public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
- String authHeader = request.getHeader("Authorization");
- if (authHeader == null || !authHeader.startsWith("Bearer ")) {
- response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized: Missing token");
- return false;
- }
- String token = authHeader.substring(7);
-
- try {
- // Validate the token using the Scalekit SDK
- TokenClaims claims = scalekitClient.token().verifyToken(token, null);
-
- // Check if the API client app has the required scope
- String requiredScope = "deploy:applications";
- if (claims.getScopes() != null && claims.getScopes().contains(requiredScope)) {
- // API client app has the required scope
- return true;
- } else {
- // API client app does not have the required scope
- response.sendError(HttpServletResponse.SC_FORBIDDEN, "Forbidden: Insufficient permissions");
- return false;
- }
- } catch (Exception e) {
- // Token is invalid or expired
- response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized: Invalid token");
- return false;
- }
- }
- }
- ```
-
-
-
-
-