Tools

Authentication reference

On this page

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 (apiKeyapi_key, tokenEndpointtoken_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.

IdentityHow it authenticatesTypical use
BuilderOAuth login → org-scoped bk_ API key stored in CLI profileblocks run, blocks publish, blocks register
Agent machine userSame bk_ API key via BLOCKS_API_KEY env var → AgentAuth JWTLong-running agent processes
Authenticated consumerAPI key, token endpoint, or token provider → consumer JWTApps calling agents on behalf of users
Embedded userBrowser OAuth popup → refresh token + scoped JWTPartner-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.

MethodHow
DashboardGo 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:

text
API key has expired  run 'blocks login' to create a new one

Rotating 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.

  1. Disable the old key in the dashboard at app.blocks.ai/manage/api-keys.
  2. Run blocks login --write-env to mint a fresh key and update .env.
  3. 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 logout does 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.

EventTime 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 revokedInstant on next request
Stream credential revokedInstant 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 logout is 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.

SituationWhat happensRecovery
Browser does not openCLI prints the auth URL — open it manuallyCopy the URL and complete the flow in a browser
Login times out (5 minutes)CLI exits with a timeout errorRun blocks login again
Wrong org selected at loginKey is scoped to the wrong orgRun blocks login again and select the correct org
Key expiredCLI exits with API key has expired on next commandRun blocks login --write-env
Key revoked remotelyAgent receives API_KEY_INVALID on next refreshRun 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.

typescript
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:

typescript
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:

ParameterValue
Max retries3
Base delay5 seconds
Maximum delay30 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:

  1. Deduplicates concurrent refresh attempts — multiple in-flight requests share a single refresh.
  2. Fetches a new token.
  3. 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_INVALID can 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

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

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:

typescript
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:

bash
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 profile

Embedded 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.

CodeHTTPSDK behaviorRecovery
API_KEY_INVALID401Throws AgentAuthFatalError (agent) or Error (consumer)Re-authenticate with blocks login --write-env and restart
REFRESH_TOKEN_INVALID401Agent: re-connects with stored API key. Consumer: re-bootstraps from API key or token endpointTransparent for API key mode; surfaces as AuthRefreshFailedError if re-bootstrap also fails
EMBEDDED_JWT_REVOKED401SDK refreshes the embed JWT and retriesTransparent — handled by the embed widget
EMBEDDED_JWT_LOGOUT401SDK detects the user has signed out; refreshes and retriesIf refresh fails, call signInAndGetClients() to re-authenticate
EMBEDDED_JWT_SCOPE_DRIFT401SDK refreshes to re-sync the agent scopeTransparent — handled by the embed widget
EMBEDDED_JWT_KILLED401SDK narrows agent scope on refreshTransparent — but the killed agent is no longer callable
AGENT_OUT_OF_SCOPE403Throws on RPC callEnsure the agent was included in the original signInAndGetClients() call
ACTIVE_ORG_REQUIRED400Throws on consumer-token exchangePass X-Active-Org header (session callers only)
ACTIVE_ORG_INVALID400Throws on consumer-token exchangeEnsure the org ID is a valid UUID
ACTIVE_ORG_FORBIDDEN403Throws on consumer-token exchangeUser 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.