NullSpend Docs

Errors

Standard error response format and complete catalog of error codes returned by the NullSpend proxy and dashboard API.

Every error response from NullSpend — both the proxy and the dashboard API — uses the same format:

{
  "error": {
    "code": "budget_exceeded",
    "message": "Request blocked: estimated cost exceeds remaining budget",
    "details": null
  }
}
  • code — Machine-readable error identifier. Use this for programmatic handling.
  • message — Human-readable explanation.
  • details — Additional context (object or null). Present on validation errors, session limits, and velocity limits.

Proxy Errors

These errors are returned by the NullSpend proxy (proxy.nullspend.dev).

Authentication

CodeHTTPWhenFix
unauthorized401X-NullSpend-Key header is missing, malformed, or the key has been revokedVerify the header is present and the key is active in Settings

Budget Enforcement

CodeHTTPWhenFix
budget_exceeded429Estimated cost of this request exceeds the remaining budgetIncrease the budget ceiling, wait for the period to reset, or remove the budget
customer_budget_exceeded429Estimated cost exceeds the customer-level budget limitIncrease the customer budget or remove the limit. The response details includes customer_id, budget_limit_microdollars, and budget_spend_microdollars
velocity_exceeded429Spend rate within the velocity window exceeds the configured limitWait for the cooldown period (check Retry-After header). The response details includes limitMicrodollars, windowSeconds, and currentMicrodollars
session_limit_exceeded429Cumulative spend for this session ID exceeds the session limitStart a new session (new X-NullSpend-Session value) or increase the session limit. The response details includes session_id, session_spend_microdollars, and session_limit_microdollars
tag_budget_exceeded429Estimated cost exceeds a tag-level budget limitAdjust the tag budget. The response details includes tag_key, tag_value, budget_limit_microdollars, and budget_spend_microdollars

Per-Customer Budgets (/v1/bind, /v1/gate, /v1/customers/:id/unit-economics)

CodeHTTPEndpointWhenFix
invalid_customer_id400bind, gate, unit-economicscustomerId missing, malformed (non-[a-zA-Z0-9._:-]+), or exceeds 256 charsSend a valid customer ID
invalid_plan_ref400bindplanRef missing or exceeds 256 charsProvide a non-empty plan label
invalid_budget_cap400bindbudgetCap not a non-negative integer (microdollars)Send an integer 0 or greater
invalid_margin_target400bindmarginTargetPercent outside 0–100Send an integer between 0 and 100 (or omit)
invalid_estimate400gateestimatedCostMicrodollars not a positive integer (zero rejected)Send a positive integer microdollar estimate
invalid_feature400gatefeature empty or exceeds 256 charsSend a non-empty string up to 256 chars (or omit)
invalid_idempotency_key400bind, gateIdempotency-Key exceeds 256 chars or contains non-printable-ASCIIUse a printable-ASCII key under 256 chars
customer_data_unsupported400bindcustomer_data / customerData field present (Sprint 2 scope)Remove the field; revisit in Sprint 2 when Stripe revenue sync ships
customer_not_allowed403bind, gatecustomerId outside the API key's allowedCustomers scopeUse a key whose scope includes this customer, or remove allowedCustomers from the key
not_found404unit-economicsCustomer not bound under the auth-resolved org (also covers cross-org / disallowed-customer to prevent enumeration)Verify the customer is bound under your org
binding_inactive409bindExisting binding has status = "suspended" or "archived"; reactivation requires admin path (Sprint 2)Reactivate via admin path (when shipped) or use a new customer ID
idempotency_conflict409bind, gateSame Idempotency-Key reused with a different request bodyUse a fresh idempotency key for the new request
idempotency_unavailable503bind, gatePostgres-side idempotency persistence failedRetry without Idempotency-Key, or retry later. The route fails closed so the caller knows the request is not safely retryable with the original key

See Unit Economics API for the full request/response shapes and the Per-Customer Budgets guide for the conceptual model. | loop_detected | 429 | Repeated identical or similar prompts exceeded the loop-detection threshold within the sliding window | Inspect agent logic for retry loops. Adjust thresholds in budget settings, or set loop_max_calls to disable. Retry-After returns the recommended cooldown | | plan_limit_exceeded | 429 | Org exceeded its NullSpend plan-tier governed-request cap (Free tier hard-blocks today) | Upgrade via the upgrade_url returned in the body, or wait for the next period reset. Top-level error.upgrade_url and error.self_host_url are set on this denial | | budget_unavailable | 503 | Budget enforcement service is temporarily unavailable | Retry after a brief delay. The proxy fails closed — requests are blocked, not passed through |

Policy (Mandates)

CodeHTTPWhenFix
mandate_violation403The request's model or provider is not in the API key's allowed_models / allowed_providers policyUse a model/provider on the allow list. The response details includes mandate ("allowed_models" or "allowed_providers"), requested, and allowed

Request Validation

CodeHTTPWhenFix
bad_request400Request body is not valid JSON or is missing required fieldsCheck the request body format
invalid_model400The model field is not in the pricing catalogCheck supported models
payload_too_large413Request body exceeds 1 MBReduce the request body size
invalid_upstream400X-NullSpend-Upstream URL is not in the allowlistUse the default upstream or contact support to add your URL
invalid_estimate422The cost estimator could not produce a finite cost (e.g., negative or non-finite max_tokens)Check that max_tokens / max_completion_tokens are positive finite integers

Rate Limiting

CodeHTTPWhenFix
rate_limited429Too many requests. Default limits: 120/min per IP, 600/min per API keyReduce request rate. Check the Retry-After header for when to retry

Rate limit responses include X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, and Retry-After headers. See the headers reference. For the full rate limiting reference, see Rate Limits.

Upstream & Server

CodeHTTPWhenFix
upstream_error502The upstream provider returned an error or no response bodyCheck the provider's status page (e.g., status.openai.com)
not_found404The requested endpoint is not supported by the proxySupported endpoints: POST /v1/chat/completions (OpenAI), POST /v1/messages (Anthropic), POST /v1/mcp/budget/check, POST /v1/mcp/events
internal_error500Unexpected server errorRetry the request. If persistent, contact support with the X-NullSpend-Trace-Id from the response

Dashboard API Errors

These errors are returned by the NullSpend dashboard API (nullspend.dev/api/).

Validation

CodeHTTPWhenFix
invalid_json400Request body is not valid JSONSend valid JSON with Content-Type: application/json
validation_error400Request failed schema validationCheck details.issues for specific field errors
unsupported_media_type415Content-Type is not application/jsonSet the Content-Type header
payload_too_large413Request body exceeds the max sizeReduce the request body

Validation error details include an issues array:

{
  "error": {
    "code": "validation_error",
    "message": "Request validation failed.",
    "details": {
      "issues": [
        { "path": ["amount"], "message": "Expected number, received string" }
      ]
    }
  }
}

Resources

CodeHTTPWhenFix
not_found404The requested resource does not existVerify the resource ID
limit_exceeded409Resource limit reached for the organization's tier (e.g., Free: 10 keys, 2 webhooks, 3 budgets)Delete unused resources or upgrade to a higher tier

HITL Actions

CodeHTTPWhenFix
invalid_action_transition409Invalid state transition (e.g., approving an already-rejected action)Check the action's current state before transitioning
stale_action409The action was modified by another actor since you last fetched itRe-fetch the action and retry
action_expired409The action's TTL has expiredCreate a new action

Rate Limiting

CodeHTTPWhenFix
rate_limit_exceeded429Too many requests (per-IP or per-key)Reduce request rate. Check the Retry-After header

Note: The proxy uses rate_limited while the dashboard API uses rate_limit_exceeded. Handle both codes if your application calls both services.

Authentication & Authorization

CodeHTTPWhenFix
authentication_required401No valid session or API keyLog in or provide a valid X-NullSpend-Key header
forbidden403Authenticated but not authorized to access this resourceVerify you own the resource

Server

CodeHTTPWhenFix
service_unavailable503A downstream service is temporarily unavailableRetry after a brief delay
internal_error500Unexpected server errorRetry the request

HTTP Status Code Summary

StatusMeaning
400Bad request — check your input
401Identity unknown — check your API key or session
403Identity known but not authorized
404Resource or endpoint not found
409Conflict — resource limit or state conflict
413Request body too large
415Wrong content type
429Rate or budget limit exceeded — check Retry-After
500Server error — retry
502Upstream provider error
503Service temporarily unavailable — retry

Handling Errors Programmatically

const response = await fetch("https://proxy.nullspend.dev/v1/chat/completions", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.OPENAI_API_KEY}`,
    "X-NullSpend-Key": process.env.NULLSPEND_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ model: "gpt-4o", messages: [{ role: "user", content: "Hello" }] }),
});

if (!response.ok) {
  const { error } = await response.json();
  switch (error.code) {
    case "budget_exceeded":
      // Notify the user, queue for later, or request budget increase
      break;
    case "velocity_exceeded":
      // Back off and retry after Retry-After seconds
      const retryAfter = response.headers.get("Retry-After");
      break;
    case "rate_limited":
      // Reduce request rate
      break;
    case "loop_detected":
      // Stop the agent loop — repeated calls were blocked
      break;
    case "plan_limit_exceeded":
      // Surface the upgrade CTA
      console.log(`Plan cap hit. Upgrade: ${error.upgrade_url}`);
      break;
    case "mandate_violation":
      // Switch to an allowed model/provider
      console.log(`Allowed: ${error.details?.allowed?.join(", ")}`);
      break;
    default:
      console.error(`NullSpend error: ${error.code} — ${error.message}`);
  }
}

API Reference

On this page