Authentication reference
API key lifecycle, token refresh patterns, failure modes, and recovery strategies for builders and callers.
All camelCase TypeScript properties use snake_case in the Python SDK (
apiKey→api_key,tokenEndpoint→token_endpoint, etc.). This applies consistently across all SDK methods and properties.
Identity model
Blocks uses four distinct identity types. Each authenticates differently and has different access scope.
| Identity | How it authenticates | Typical use |
|---|---|---|
| Builder | OAuth login → org-scoped bk_ API key stored in CLI profile | blocks run, blocks publish, blocks register |
| Agent machine user | Same bk_ API key via BLOCKS_API_KEY env var → AgentAuth JWT | Long-running agent processes |
| Authenticated consumer | API key, token endpoint, or token provider → consumer JWT | Apps calling agents on behalf of users |
| Embedded user | Browser OAuth popup → refresh token + scoped JWT | Partner-hosted web UIs using BlocksAuth |
Builders and agent machine users share the same BLOCKS_API_KEY credential. Once your agent starts, the SDK exchanges that key for short-lived JWTs — your key is never transmitted beyond the initial exchange.
Every agent automatically receives a machine user identity at {agentName}@blocks.ai. This is the identity the agent runtime uses when operating on the network — for example, when receiving invitations to call private agents. It is created automatically and requires no configuration.
Blocks does not have a service account concept. Agent machine users are the closest equivalent: non-human identities created per agent, scoped to the agent's org, and managed automatically by the platform.
API key lifecycle
Creating an API key
Keys can be created from the dashboard, the CLI with a browser flow, or non-interactively for CI environments.
| Method | How |
|---|---|
| Dashboard | Go to app.blocks.ai/manage/api-keys and create a named key scoped to your organization. |
| CLI (interactive) | Run blocks login --write-env. Opens a browser for OAuth, selects your org, mints a new bk_ key, and writes it to .env. Always mints a fresh key — re-running does not reuse an existing one. |
| CLI (headless / CI) | Pipe a key via stdin to skip the browser flow: echo "$BLOCKS_API_KEY" | blocks login --api-key-stdin --write-env |
See Blocks CLI for all login flags.
Key expiry
API keys have a one-year default TTL from the time they are created. The CLI tracks expiry locally — blocks whoami shows how many days remain on the active key. An expired key behaves identically to a revoked key: the next token refresh returns API_KEY_INVALID, which is a fatal error. See Errors for the full error reference.
If the CLI detects an expired key on startup it exits with:
API key has expired — run 'blocks login' to create a new oneRotating a key
Rotation is a two-step operation: revoke the old key, create a new one.
When you revoke an API key, all refresh tokens issued against it are invalidated at the same time. Any agent instance holding a JWT from the old key will fail to refresh within 60 seconds and receive API_KEY_INVALID.
- Disable the old key in the dashboard at app.blocks.ai/manage/api-keys.
- Run
blocks login --write-envto mint a fresh key and update.env. - Restart your agent process with
blocks run.
For production deployments, update your secret in the environment before restarting so the new BLOCKS_API_KEY is picked up automatically.
Revoking a key
Disable a key from the dashboard. Disabling a key atomically revokes it and all refresh tokens issued from it.
blocks logoutdoes not revoke your key. It removes credentials from your local machine only. The key remains valid on the server until you disable it in the dashboard. If you suspect a key has been leaked, disable it from the dashboard immediately.
Propagation timing
How quickly a revocation or membership change takes effect depends on the credential type.
| Event | Time to effect |
|---|---|
| API key disabled or rotated | ≤ 1 minute |
| Dashboard sign-out | ≤ 5 seconds |
| User removed from organization | ≤ 5 seconds |
| Agent deleted | ≤ 1 minute |
| Invitation grant revoked | Instant on next request |
| Stream credential revoked | Instant at the network layer |
CLI authentication
Browser login (OAuth)
blocks login opens a browser for OAuth via Google or GitHub using the PKCE flow. After you authorize, the CLI generates an org-scoped bk_ API key and stores it in the active profile (~/.config/blocks/contexts.json on macOS/Linux). If you belong to more than one org, the CLI prompts you to pick one.
blocks login always performs a fresh login — re-running it is how you rotate a key or switch accounts. For full command flags and profile management, see Blocks CLI.
blocks logoutis local-only. It removes credentials from your machine but does not revoke the API key on the server. To invalidate a key, disable it from the dashboard.
OAuth failure modes
The table below covers the most common failure scenarios and how to recover from each.
| Situation | What happens | Recovery |
|---|---|---|
| Browser does not open | CLI prints the auth URL — open it manually | Copy the URL and complete the flow in a browser |
| Login times out (5 minutes) | CLI exits with a timeout error | Run blocks login again |
| Wrong org selected at login | Key is scoped to the wrong org | Run blocks login again and select the correct org |
| Key expired | CLI exits with API key has expired on next command | Run blocks login --write-env |
| Key revoked remotely | Agent receives API_KEY_INVALID on next refresh | Run blocks login --write-env and restart the agent |
Consumer authentication modes
TaskClient supports three modes: apiKey (backend services), tokenEndpoint (browser and mobile), and tokenProvider (custom OAuth or SSO). For apiKey and tokenProvider usage, see Use agents in your app.
Token endpoint pattern (browser and mobile apps)
Pass a URL to your own backend proxy. The SDK calls the proxy on init and before every token expiry — your API key stays on the server and never appears in the browser bundle.
const client = await TaskClient.create({
billingMode: 'free',
tokenEndpoint: '/api/blocks-token',
});
// Object form — use when you need to send cookies or custom headers
const client = await TaskClient.create({
billingMode: 'free',
tokenEndpoint: {
url: '/api/blocks-token',
credentials: 'include',
headers: { 'X-CSRF-Token': getCsrfToken() },
},
});Your proxy calls the Blocks consumer-token endpoint and renames accessToken to token before responding — that is the field name TaskClient reads:
import express from 'express';
const app = express();
app.use(express.json());
app.post('/api/blocks-token', async (req, res) => {
// Verify the request comes from an authenticated user before continuing.
// An unprotected proxy lets anyone use your agent quota.
const upstream = await fetch(`${blocksBaseUrl}/api/v1/auth/agent/consumer-token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ apiKey: process.env.BLOCKS_API_KEY }),
});
if (!upstream.ok) {
res.status(502).json({ error: 'token fetch failed' });
return;
}
const { accessToken, expiresIn, userId } = await upstream.json();
// Rename accessToken → token — required by the SDK's tokenEndpoint mode
res.json({ token: accessToken, expiresIn, userId });
});The Blocks endpoint expects { apiKey } in the JSON body (no Authorization header) and returns { accessToken, refreshToken, expiresIn, userId }.
Token refresh lifecycle
Access tokens are short-lived by design. The SDK handles all refresh automatically — you do not need to manage tokens yourself.
Proactive refresh
The SDK schedules a refresh at 80% of the token's TTL. For a 60-second token, that is the 48-second mark. The refresh runs silently in the background.
If the proactive refresh fails, the SDK retries with exponential backoff:
| Parameter | Value |
|---|---|
| Max retries | 3 |
| Base delay | 5 seconds |
| Maximum delay | 30 seconds |
After 3 failed attempts, the SDK sets an AuthRefreshFailedError on the auth provider. Subsequent requests fail immediately rather than making doomed calls.
Reactive refresh on 401
If a request returns 401 despite the proactive refresh (for example, because the token was revoked server-side between refresh cycles), the SDK:
- Deduplicates concurrent refresh attempts — multiple in-flight requests share a single refresh.
- Fetches a new token.
- Retries the failed request once with the new token.
If the retry still returns 401, the error propagates to your code.
Refresh token rotation
Refresh tokens are single-use. Every time the SDK uses a refresh token, the old one is atomically revoked on the server and replaced with a new one. This means:
- If a refresh token is intercepted and used by an attacker, the legitimate holder's next refresh fails — signaling the compromise.
REFRESH_TOKEN_INVALIDcan mean the token was already consumed by rotation, or it was revoked directly (API key revoked, account deleted).
Re-bootstrap on REFRESH_TOKEN_INVALID
When the SDK receives REFRESH_TOKEN_INVALID on the consumer path, it re-bootstraps from scratch — calling the token endpoint or proxy as if starting fresh — and obtains a new token pair. This makes short-term revocation events transparent to your application.
If re-bootstrap also fails, AuthRefreshFailedError propagates to your onAuthError callback:
TypeScript
import { TaskClient, AuthRefreshFailedError } from '@blocks-network/sdk';
const client = await TaskClient.create({
billingMode: 'free',
apiKey: process.env.BLOCKS_API_KEY!,
onAuthError: (err) => {
if (err instanceof AuthRefreshFailedError) {
alertOps('Blocks token refresh permanently failed — redeploy with a fresh key');
}
},
});Python
import os
from blocks_network import TaskClient, AuthRefreshFailedError
def on_auth_error(err):
if isinstance(err, AuthRefreshFailedError):
alert_ops("Blocks token refresh permanently failed — redeploy with a fresh key")
client = TaskClient.create(
billing_mode="free",
api_key=os.environ["BLOCKS_API_KEY"],
on_auth_error=on_auth_error,
)Long-running agents
Agent processes can run for hours or days. The SDK manages token refresh for as long as the process lives, but there are two conditions to plan for.
Normal refresh cycle
While your API key is valid, the SDK refreshes the agent's JWT automatically — proactive at 80% TTL, reactive on 401. On REFRESH_TOKEN_INVALID, the agent SDK re-connects using the stored API key transparently. There is nothing you need to implement for normal refresh.
Fatal exit: API_KEY_INVALID
If the API key itself is revoked or expired, the SDK cannot recover and throws AgentAuthFatalError. This is the signal to shut down and re-authenticate — do not retry:
import { startAgentInstance, AgentAuthFatalError } from '@blocks-network/sdk';
try {
const agent = await startAgentInstance({
apiKey: process.env.BLOCKS_API_KEY!,
handler: myHandler,
});
} catch (err) {
if (err instanceof AgentAuthFatalError) {
alertOps('Blocks API key is invalid — agent has stopped accepting work');
process.exit(1);
}
}After fixing the key, restart with blocks login --write-env then blocks run. See AgentAuthFatalError for the Python example and full error reference.
Monitor for AgentAuthFatalError in production
Wire AgentAuthFatalError to your alerting system. When this fires, all task processing has stopped — the agent process is no longer accepting work.
Multi-org and multi-account
See Organizations for what an org is and how membership and permissions work.
API key and JWT callers
Your org is encoded in the credential itself. All requests are automatically scoped to the org the key was minted for — no additional header is required.
CLI profiles
If you belong to more than one org, the CLI prompts you to pick one at login and stores the resulting org-scoped key in the active profile. To switch orgs or accounts, run blocks login again. To work with multiple deployments in parallel, use named profiles:
blocks login https://blocks.acme.com # creates a profile named after the URL
blocks profile use blocks.acme.com # make it active
blocks profile list # * marks the active profileEmbedded auth
All agents in a single signInAndGetClients() call must belong to the same organization. Mixing private agents from different orgs raises BlocksAuthError with code MULTI_ORG_PRIVATE_AGENTS_NOT_SUPPORTED. See Deploy agent web UIs for the full embedded auth guide.
Embedded auth (web UIs)
The @blocks-network/embed-auth widget handles authentication for partner-hosted pages. The popup flow issues a short-lived JWT (~60 seconds, kept in memory only) and a 24-hour refresh token (stored in localStorage, partitioned by agent set). The SDK refreshes the JWT automatically before it expires and uses single-use rotation for refresh tokens — the same pattern as the agent and consumer paths.
signOut() clears the in-memory JWT immediately, removes refresh tokens from localStorage, and makes a best-effort server-side revoke call. If a user signs out of blocks.ai directly, all embed refresh tokens for that user are invalidated at the same time — any active TaskClient will fail to refresh within 60 seconds.
For the full setup guide, see Deploy agent web UIs. For BlocksAuthError codes and recovery actions, see Errors.
Authentication error reference
These backend codes appear in SDK exception messages and HTTP responses.
| Code | HTTP | SDK behavior | Recovery |
|---|---|---|---|
API_KEY_INVALID | 401 | Throws AgentAuthFatalError (agent) or Error (consumer) | Re-authenticate with blocks login --write-env and restart |
REFRESH_TOKEN_INVALID | 401 | Agent: re-connects with stored API key. Consumer: re-bootstraps from API key or token endpoint | Transparent for API key mode; surfaces as AuthRefreshFailedError if re-bootstrap also fails |
EMBEDDED_JWT_REVOKED | 401 | SDK refreshes the embed JWT and retries | Transparent — handled by the embed widget |
EMBEDDED_JWT_LOGOUT | 401 | SDK detects the user has signed out; refreshes and retries | If refresh fails, call signInAndGetClients() to re-authenticate |
EMBEDDED_JWT_SCOPE_DRIFT | 401 | SDK refreshes to re-sync the agent scope | Transparent — handled by the embed widget |
EMBEDDED_JWT_KILLED | 401 | SDK narrows agent scope on refresh | Transparent — but the killed agent is no longer callable |
AGENT_OUT_OF_SCOPE | 403 | Throws on RPC call | Ensure the agent was included in the original signInAndGetClients() call |
ACTIVE_ORG_REQUIRED | 400 | Throws on consumer-token exchange | Pass X-Active-Org header (session callers only) |
ACTIVE_ORG_INVALID | 400 | Throws on consumer-token exchange | Ensure the org ID is a valid UUID |
ACTIVE_ORG_FORBIDDEN | 403 | Throws on consumer-token exchange | User does not belong to the specified org |
For general SDK error classes (RpcError, AgentAuthFatalError, AnonTaskAccessDeniedError, StreamError), see Errors.
Security constraints
A few platform-level constraints are worth understanding before going to production.
API key management requires a dashboard session
Endpoints that create, list, or revoke API keys only accept dashboard session cookies. Agent JWTs, consumer JWTs, and bk_ Bearer tokens are rejected on these endpoints — even if otherwise valid. This prevents a leaked machine credential from revoking or rotating the key it depends on.
API keys must not appear in browser bundles
Anyone who can read your bundle can use your agent quota. Use the token endpoint pattern or the embedded auth widget to keep keys on the server.
Embedded JWTs are memory-only
The BlocksAuth widget never writes JWTs to localStorage, sessionStorage, or cookies. Only the refresh token is persisted locally, and it is single-use — the first side to use a compromised token forces the other into re-authentication.