Errors Reference
SDK methods reject with ConfigureError for API, validation, permission, tool, timeout, and network failures.
All docs pages, indexed for agents: https://docs.configure.dev/llms.txt (add .md to any page URL for plain markdown).
ts
import { ConfigureError, ErrorCode } from "configure";
try {
await profile.executeTool(toolCall);
} catch (error) {
if (error instanceof ConfigureError) {
console.error(error.code, error.statusCode, error.type, error.requestId);
}
}Error Codes
ErrorCode exports the values below. Each heading is the verbatim string on error.code.
Recovery in one line each, so a branch can be written from this table alone:
error.code | Recover with | Retry? |
|---|---|---|
API_KEY_MISSING | npx configure setup, or pass apiKey to the constructor | No |
AUTH_REQUIRED | configure.auth.signInUrl() and send the user through it; discard the dead token | No |
INVALID_INPUT | Fix the field named by error.param | No |
TOOL_NOT_CONNECTED | profile.connect({ app }) and show connect_url verbatim | Only after the user connects |
NETWORK_ERROR | Retry only if the call was idempotent; branch on the code, retryable is undefined | Idempotent calls only |
RATE_LIMITED | Wait error.retryAfter seconds, then retry | Yes, when retryable is true |
NOT_FOUND | Fix the id. Also check statusCode === 404, which catches structured 404s too | No |
SERVER_ERROR | Retry with backoff; report error.requestId if it persists | Yes |
TIMEOUT | Retry only if the call was idempotent; branch on the code, retryable is undefined | Idempotent calls only |
ACCESS_DENIED | profile.tools({ connectors, actions }) with the options you intend to execute | After enabling |
TOOL_ERROR | Show error.message; retry once when error.retryable === true | Conditionally |
PAYMENT_REQUIRED | Send the user to their plan. Backoff does not help | No |
COMMIT_REQUIRED | profile.commit({ messages }), then repeat the blocked call | Only after committing |
API_KEY_MISSING
Cause: no API key was provided and CONFIGURE_API_KEY was not set.
Fix: pass apiKey when constructing the client, or set CONFIGURE_API_KEY. npx configure setup writes it to .env alongside CONFIGURE_PUBLISHABLE_KEY and CONFIGURE_AGENT.
AUTH_REQUIRED
Cause: token is missing, invalid, expired, or rejected by the API.
Fix: send the user through the hosted flow and stop reusing the token. Retrying with the same token fails identically.
ts
if (error.code === "AUTH_REQUIRED") {
return reply(`Sign in again: ${configure.auth.signInUrl()}`);
}For message agents that need sender binding, createMessageSignInUrl() takes reason: "signin" | "reconnect" | "permissions". Over MCP, configure_connect returns the same link and needs no authorization. See Auth Flows.
INVALID_INPUT
Cause: SDK-side or API-side input validation failed. Cases that arrive as INVALID_INPUT:
- Missing linked
tokenor app-localexternalId. - Invalid or reserved agent handle.
- Malformed
forgetid (anything other thanmem_plus 32 hex characters). An unknown id or another agent's memory does not throw at all;forget()resolves with{ deleted: false, message }because deletion is own-memories only. Checkresponse.deleted, not just the catch block. - Structured backend 404s (
resource_missing,agent_not_found):INVALID_INPUTwithstatusCode404, notNOT_FOUND. Branch onstatusCode === 404to catch both structured and legacy 404s.
Fix: correct the input; when set, error.param names the offending field.
TOOL_NOT_CONNECTED
Cause: a connector-backed tool requires a user connection that is not available (missing connector connection). A dead grant and an account that was never connected both land here.
Fix: mint a link and show it verbatim. Never construct one yourself.
ts
const { connect_url } = await profile.connect({ app: "gmail" });
return reply(`Connect Gmail here: ${connect_url}`);Do not retry until the user says they finished. error.suggestedAction is "connect_tool" when the app was never connected and "reconnect" when the grant died, which is the difference between rendering "Connect Gmail" and "Reconnect Gmail". See Connector repair.
NETWORK_ERROR
Cause: fetch failed before an API response was received.
Fix: retry with backoff ONLY when the call was idempotent. A read is safe to repeat. A write is not: the SDK cannot tell whether the server applied it before the connection died, and there is no idempotency key, so a blind retry can save or send twice. retryable is undefined here by design; branch on error.code.
RATE_LIMITED
Cause: the API returned a rate limit response. OTP throttling (otp_blocked) also surfaces as RATE_LIMITED, but with retryable: false.
Fix: wait error.retryAfter seconds (set by the backend on rate limits), then retry. When retryAfter is undefined, back off on your own schedule.
NOT_FOUND
Cause: the requested resource was not found. Reachable only from legacy unstructured responses; structured backend 404s arrive as INVALID_INPUT with statusCode 404.
Fix: correct the id, and branch on error.statusCode === 404 rather than on the code, so both shapes are caught.
SERVER_ERROR
Cause: the backend returned a 5xx or structured API error. It is also the fallback for any backend error type the SDK does not recognize.
Fix: retry with backoff. Report error.requestId if it persists.
TIMEOUT
Cause: the request timed out (the fetch was aborted).
Fix: retry with backoff ONLY when the call was idempotent. A read is safe to repeat. A write is not: the SDK cannot tell whether the server applied it before the connection died, and there is no idempotency key, so a blind retry can save or send twice. retryable is undefined here by design; branch on error.code.
ACCESS_DENIED
Cause: the current profile object is not allowed to access the requested resource or tool. Any tool name not in the enabled set, unknown or not, rejects with this code: executeTool() checks the enabled set first, so the INVALID_INPUT unknown-tool branch at dispatch is practically unreachable.
Fix: call profile.tools() with the same connector and action options you intend to execute. See Tool Rejection Behavior.
TOOL_ERROR
Cause: a connector or provider operation failed. This is the provider's own failure, not a connection problem.
Fix: surface error.message to the user. Retry once when error.retryable === true; the backend marks transient tool failures retryable.
PAYMENT_REQUIRED
Cause: billing or quota limits blocked the request (quota_exceeded, HTTP 402). Backoff never clears it.
Fix: send the user to their plan. The REST envelope carries error.upgrade_url on this code; the SDK does not expose it on ConfigureError, so read it off the response body if you need the exact link.
COMMIT_REQUIRED
Cause: a profile read needs a follow-up commit.
Fix: commit first, then repeat the call that was blocked.
ts
await profile.commit({
messages: [
{ role: "user", content: userText },
{ role: "assistant", content: replyText },
],
});
await profile.read(); // now succeedsCommit needs at least one of messages, memories, or toolResults with real content, or it throws INVALID_INPUT before any request goes out. On the hosted MCP surface the model calls configure_profile_commit instead. Repeating the blocked call without committing fails the same way. The model-facing commit tool is not in the default tool set; it exists only for MCP and adapter runtimes via tools({ advanced: { commit: true } }).
Structured Fields
ConfigureError exposes fields directly:
codestatusCodetypeparamretryablesuggestedActiondocUrlretryAfterrequestId
There is no status or details property on ConfigureError.
retryable is set only from structured backend responses ({ error: { type, code, retryable, ... } }). The backend marks the plain rate-limit code, transient tool failures, and 5xx-class errors as retryable: true. OTP throttling (otp_blocked) also surfaces as RATE_LIMITED, but with retryable: false.
Three cases leave it undefined rather than false:
TIMEOUTandNETWORK_ERROR, which the SDK builds locally from a caught exception and never decorates with backend details.- Any response whose body is a flat error string instead of the structured object. That path maps by HTTP status alone, so
retryable,retryAfter, andrequestIdare all absent even on a 429 or a 5xx. - Anything passed through
classifyError(), which rebuilds the error with a friendly message and keeps onlycodeandstatusCode.
So a retry loop gated on error.retryable === true never retries a local timeout or a dropped connection. Gate on the code for those, and on retryable for server-sourced failures:
ts
const RETRY_BY_CODE = new Set(["TIMEOUT", "NETWORK_ERROR"]);
const willRetry = RETRY_BY_CODE.has(error.code) || error.retryable === true;
const waitMs = (error.retryAfter ?? 0) * 1000 || backoff(attempt);retryAfter is a number of seconds to wait before retrying. The backend sets it on rate limits; otherwise it is undefined.
suggestedAction is a closed machine-actionable enum, not free text: reauthenticate, fix_request, connect_tool, reconnect, retry, upgrade_plan, specify_agent, use_secret_key, check_permissions, use_hosted_auth, complete_approval, or commit_profile. Use it as the programmatic hook for recovery branches.
connect_tool and reconnect both arrive as TOOL_NOT_CONNECTED. The first means the user never connected the app; the second means a classified dead grant. Both are fixed by one trip through profile.connect({ app }), but only the second should say "Reconnect".
Tool Rejection Behavior
Call profile.tools() with the same connector and action options you intend to execute. tools() is synchronous and returns ConfigureToolDefinition[]; do not await it. tools() records the enabled names; profile.executeTool(toolCall: ConfigureToolCall): Promise<unknown> rejects any name outside that recorded set with ACCESS_DENIED. Before any tools() call the set holds the six default tools (read, search, remember, forget, import, connect). toolCall accepts { name, arguments }, { name, input }, or { function: { name, arguments } }; arguments can be a JSON string.
ts
profile.tools({ connectors: ["gmail"] });
await profile.executeTool({
name: "configure_email_send",
arguments: {},
}); // throws ConfigureError with code ACCESS_DENIEDMCP (JSON-RPC) errors
On the hosted MCP surface (https://mcp.configure.dev), tool failures arrive as JSON-RPC error objects, not ConfigureError. The machine-readable part rides in error.data: data.code names the failure, and when a minted link can fix it, data.suggested_action is an object carrying that link.
-32009(commit_required): a profile read or search created a commit obligation that has not been cleared. Recover by callingconfigure_profile_commitwith the bounded messages or tool results from this turn, then retry the blocked call. Neverconfigure_connect(that is for sign-in), and never a bare retry.-32010(mcp_commit_plumbing_required): the transport never declared commit support. Recover by sendingX-Configure-Session-Id,X-Configure-Runtime-Scope-Id, andX-Configure-MCP-Commit-Supported: 1on every request; the ids are yours to choose and only have to be stable. Full contract: message agents.-32001: no user was resolved for the call. Recover by sendingAuthorization: Bearer <token>for a linked user orX-User-Idfor an unlinked one.configure_connectis the one tool that answers without either.-32000: everything else, withdata.codenaming it. Refused connector calls land here (data.codeis, for example,tool_not_connectedorpermission_needed). Recover by replying withdata.connect_urlverbatim;data.next_tool_callnames the call that mints a fresh one (configure_connect, carryingappwhen a single app applies). Do not retry until the user has acted on it.
ts
const data = rpcError.data ?? {};
if (data.connect_url) return reply(`Connect it here: ${data.connect_url}`);