Errors and refusals
Why a governed call gets refused, and how to handle each refusal in code.
Status codes
| Status | Means | Body |
|---|---|---|
401 | No credential, or the credential is wrong, rotated, or revoked. | {"detail":"unknown api key"} |
402 | A workspace, per-key or per-agent budget was exceeded. The call never reached the provider. | {"detail":{"code":"budget_exceeded",...}} — see below |
403 | Authenticated, but the key's scope, model allowlist, or tool binding does not cover this request. | {"detail":{"error":"model_not_allowed","model":...,"allowed_models":[...]}} |
400 | The request is invalid, for example streaming an image-generation call. | {"detail":{"code":"invalid_request",...}} |
429 | A rate limit (requests per minute) or tool-call limit (calls per window) was hit. | {"detail":{"code":"rate_limit_exceeded",...}} |
409 | The resource isn't in the right state, for example deleting something that must be revoked first. Fix its state; don't retry the same call. | {"detail":"only a revoked server can be deleted — revoke it first"} |
401 — unauthorized: bad credential or paused agent
{"detail":"unknown api key"}The key is missing, malformed, rotated or revoked. After a rotation the old secret stops working immediately. Mint or rotate a key on API keys and spend. A paused or retired agent's key is unauthorized too; see Agents.
402 — budget exceeded
{
"detail": {
"code": "budget_exceeded",
"message": "monthly budget exceeded",
"limit_usd": "0.000100",
"spent_usd": "0.000385",
"attempted_cost_usd": "0.005000"
}
}A call is refused when spent + attempted would cross the cap, so you can be
refused while the dashboard still shows headroom. With $0.00542 spent against
a $0.01 cap, a $0.005 call is refused because $0.01042 is over. See
Budgets.
Per-key limits use the same shape with a different code:
key_daily_limit_exceeded and key_monthly_limit_exceeded, both with a
resets_at. A license_blocked or plan_required 402 means your plan doesn't
include the feature, not that you ran out of budget.
A token cap refusal has the code token_cap_exceeded and carries level, period, limit_tokens, used_tokens and attempted_tokens. The SDKs raise TokenCapExceededError (TokenCapExceededException in Java), a subclass of the budget error, so one handler covers both.
Budget pools add pool_budget_exceeded (the pool's limit) and pool_member_share_exceeded (the calling key's share). The body says which limit was hit and who owns it (limit_kind, pool_owner, key_owner). See Pool API and errors.
403 — model not allowed, tool not authorized, or missing scope
Three causes share this status:
- Model not allowed for this key:
{"detail":{"error":"model_not_allowed","model":...,"allowed_models":[...]}}. The key's model list doesn't include the requested model. - Tool not authorized:
{"detail":{"code":"tool_not_authorized","tool_name":...}}. The model tried to call a tool the agent isn't bound to. Repeated attempts on the same tool escalate to a429(tool_repeatedly_unauthorized). - Role/scope:
{"detail":"requires role '<role>' or one of scopes: <scopes>"}. The caller's role or scopes don't cover the action, for example reading audit without theaudit:readscope or an admin role.
To fix any of these, change the request or the key's configuration.
Refusals are recorded
Every refusal (budget, rate limit, license, model not allowed, guardrail, tool not authorized, agent paused) is written to the Refusals log with its cause. Check it first when an integration stops working after someone tightens a cap or a model allowlist.

Each cause links to the screen that controls it: budget and rate_limit link
to Budgets, tool_not_authorized to the tool registry, and
agent_paused to the agent. Refusals happen before any provider call, so a
refused request costs nothing.
Which exception to catch
All three SDKs raise a typed error for each non-2xx status, so you branch on the
exception type. An unauthorized key raises AuthenticationError in Python and
TypeScript:
402:BudgetExceededError401:AuthenticationError403:PermissionDeniedError429:RateLimitError
Java uses the same names ending in Exception, such as BudgetExceededException.
Handling refusals in code
Each example catches the refusals above.
from forgebench import Forgebench, BudgetExceededError, PermissionDeniedError, AuthenticationError
client = Forgebench(api_key="sk_...", base_url="https://api.forgebench.ai")
messages = [{"role": "user", "content": "..."}]
try:
resp = client.chat.completions.create(model="gpt-4o", messages=messages)
except BudgetExceededError as e:
log.warning("budget gate refused the call: %s", e)
except PermissionDeniedError as e:
log.error("not authorized: %s", e)
except AuthenticationError as e:
log.error("auth failed: %s", e)import { Forgebench, BudgetExceededError, PermissionDeniedError, AuthenticationError } from "@seedlinglabs/forgebench-sdk";
const forgebench = new Forgebench({ apiKey: "sk_...", baseUrl: "https://api.forgebench.ai" });
const messages = [{ role: "user" as const, content: "..." }];
try {
const res = await forgebench.chat.create({ model: "gpt-4o", messages });
} catch (e) {
if (e instanceof BudgetExceededError) {
console.warn("budget gate refused the call", e.body);
} else if (e instanceof PermissionDeniedError) {
console.error("not authorized", e.body);
} else if (e instanceof AuthenticationError) {
console.error("auth failed", e.body);
} else {
throw e;
}
}import ai.forgebench.ChatCompletionRequest;
import ai.forgebench.ChatMessage;
import ai.forgebench.Forgebench;
import ai.forgebench.errors.AuthenticationException;
import ai.forgebench.errors.BudgetExceededException;
import ai.forgebench.errors.PermissionDeniedException;
import ai.forgebench.types.ChatCompletion;
import java.util.List;
Forgebench client = Forgebench.builder().apiKey("sk_...").baseUrl("https://api.forgebench.ai").build();
List<ChatMessage> messages = List.of(ChatMessage.user("..."));
try {
ChatCompletion resp = client.chat().completions().create(ChatCompletionRequest.builder()
.model("gpt-4o")
.messages(messages)
.build());
} catch (BudgetExceededException e) {
log.warn("budget gate refused the call: {}", e.getBody());
} catch (PermissionDeniedException e) {
log.error("not authorized: {}", e.getBody());
} catch (AuthenticationException e) {
log.error("auth failed: {}", e.getBody());
}# -i prints the status line: 402 budget, 403 not authorized, 401 auth failed
curl -i -X POST https://api.forgebench.ai/v1/chat/completions \
-H "Authorization: Bearer sk_..." \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4o",
"messages": [{"role": "user", "content": "..."}]
}'
