{"openapi":"3.1.0","info":{"title":"Spendline API","version":"1.0.0","summary":"Spendline is the financial control layer for production AI spend. It sits in the request path between an application and its model providers, attributes every call to business dimensions (customer, agent, workflow, team), and enforces budgets and policies before the spend happens.","description":"Spendline is the financial control layer for production AI spend. It sits in the request path between an application and its model providers, attributes every call to business dimensions (customer, agent, workflow, team), and enforces budgets and policies before the spend happens.\n\n**Mechanism.** Request-path proxy. Applications change their provider base URL to Spendline and keep their own provider API keys. Attribution travels as HTTP headers.\n\n**Differentiator.** Financial control, not engineering observability. Spendline blocks spend before it occurs (HTTP 402), keeps an append-only ledger suitable for finance, and closes the month like any other COGS line.\n\n## Base URL rules (the most common integration mistake)\n\n- OpenAI SDK and every OpenAI-compatible client: `https://www.spendline.ai/v1`: **with** `/v1`.\n- Anthropic SDK: `https://www.spendline.ai`: **without** `/v1`. The SDK appends `/v1/messages` itself.\n\nThat asymmetry is real. Do not \"fix\" one to match the other.\n\n## Two credentials, two headers\n\n- The application's own provider key travels in `Authorization: Bearer` (OpenAI-compatible)\n  or `x-api-key` (Anthropic). Spendline forwards it upstream unchanged.\n- The Spendline key travels in `x-spendline-key` and is consumed by Spendline.\n\n## Not proxied\n\nEmbeddings, image generation, audio, and vector search are not in the request path.\nLeave those calls pointed directly at the provider.\n\n## Human authorization\n\nBilling, payment methods, provider-key storage, budget raises/deletes and month\nclose/reopen are deliberately absent from this spec: they require a human. See\nhttps://www.spendline.ai/agents/onboarding.md","contact":{"name":"Spendline","email":"fida@spendline.ai","url":"https://www.spendline.ai"}},"servers":[{"url":"https://www.spendline.ai","description":"Spendline production"}],"externalDocs":{"description":"Agent-facing documentation index","url":"https://www.spendline.ai/agents/index.md"},"tags":[{"name":"proxy","description":"The request path. Route AI traffic through these."},{"name":"verification","description":"Prove the integration works and is attributed."},{"name":"budgets","description":"Pre-spend financial controls."},{"name":"policies","description":"Model and token restrictions."},{"name":"rerouting","description":"Cost optimization in the request path."},{"name":"onboarding","description":"Agent-initiated, human-authorized provisioning."},{"name":"discovery","description":"Public, unauthenticated machine-readable descriptions."}],"security":[{"SpendlineKey":[]}],"components":{"securitySchemes":{"SpendlineKey":{"type":"apiKey","in":"header","name":"x-spendline-key","description":"The Spendline API key. NEVER send this as x-api-key. Anthropic uses that header for the Claude provider key, so the Spendline key would be forwarded upstream and rejected."}},"schemas":{"Budget":{"type":"object","description":"A hierarchical budget. The tightest applicable scope wins.","required":["scope_type","monthly_limit_usd"],"properties":{"id":{"type":"string","format":"uuid","readOnly":true},"scope_type":{"type":"string","enum":["org","team","agent","customer"],"description":"org = whole account. team = x-spendline-tags cost-centre key. agent = x-agent-id. customer = x-customer-id."},"scope_id":{"type":["string","null"],"description":"Required unless scope_type is \"org\". Must match the attribution value exactly."},"monthly_limit_usd":{"type":"number","minimum":0,"description":"Calendar-month cap in USD, UTC."},"strict_mode":{"type":"boolean","description":"true blocks over-cap calls with 402; false records only."},"require_approval_on_exceed":{"type":"boolean"},"created_at":{"type":"string","format":"date-time","readOnly":true},"updated_at":{"type":"string","format":"date-time","readOnly":true}}},"Policy":{"type":"object","required":["name","type","model"],"properties":{"id":{"type":"string","format":"uuid","readOnly":true},"name":{"type":"string"},"type":{"type":"string","enum":["model_block","model_allow","max_input_tokens","max_output_tokens"],"description":"model_block refuses the named model. model_allow is one ENTRY in an allow-list: every enabled model_allow row sharing one exact scope is one list, and a call must match some entry of every list that applies to it. Both judge the requested and the served model."},"model":{"type":"string","description":"Model name to match. Stored lowercased."},"match_mode":{"type":"string","enum":["exact","prefix","contains"],"default":"exact"},"value":{"type":["number","null"],"description":"Required positive number for token-limit types. Null for model_block and model_allow."},"enforcement":{"type":"string","enum":["block","warn"],"default":"block","description":"Ignored for model_block and model_allow, which always block."},"enabled":{"type":"boolean","default":true},"scope":{"type":["object","null"],"description":"Optional; the same object a reroute rule carries. Omit for an account-wide policy. A scoped policy applies to calls attributed to that agent / customer AND to calls carrying no attribution, so leaving x-agent-id off is never a way around it. A call attributed to a different agent or customer is not affected. Unknown keys are a 400.","properties":{"agent_id":{"type":"string"},"customer_id":{"type":"string"},"agent_name":{"type":"string"}}}}},"Reroute":{"type":"object","required":["from_model","to_model"],"properties":{"id":{"type":"string","format":"uuid","readOnly":true},"from_model":{"type":"string"},"to_model":{"type":"string","description":"Must be routable and must differ from from_model."},"rollout_percent":{"type":"number","minimum":0,"maximum":100,"default":100},"scope":{"type":["object","null"],"description":"Optional. Omit for an account-wide rule.","properties":{"agent_id":{"type":"string"},"customer_id":{"type":"string"}}}}},"Error":{"type":"object","properties":{"error":{"type":"string"},"error_type":{"type":"string"}}},"PermissionDenied":{"type":"object","description":"Returned when the credential lacks authority. A scoped CHILD key, the kind issued to an agent by the onboarding flow, can never satisfy owner/admin routes. Do not attempt to escalate; propose the change to the human instead.","properties":{"error_type":{"type":"string","example":"permission_denied"},"required":{"type":"string","example":"owner_or_admin"},"error":{"type":"string"}}}}},"paths":{"/v1/chat/completions":{"post":{"tags":["proxy"],"operationId":"proxyChatCompletions","summary":"OpenAI-compatible chat completions, governed and recorded","description":"Set the OpenAI SDK baseURL/base_url to https://www.spendline.ai/v1. WITH /v1. The SDK appends /chat/completions itself.\n\nProviders reachable on this shape: openai, google, xai, mistral, deepseek, qwen, together, fireworks, groq.\nThe model name selects the provider (gpt-* → OpenAI, gemini-* → Google, grok-* → xAI, mistral-* → Mistral, …).\n\nStreaming is supported. Request/response bodies are the provider's own. Spendline\ndoes not reshape them.","parameters":[{"name":"Authorization","in":"header","required":true,"description":"The application's OWN provider key, as `Bearer <key>`. Forwarded upstream.","schema":{"type":"string"}},{"name":"x-agent-id","in":"header","required":false,"description":"Stable name of the bot/agent/feature making the call, e.g. support-bot. One per agent. Not per request. Required when attribution enforcement is enabled.","schema":{"type":"string"}},{"name":"x-customer-id","in":"header","required":false,"description":"The end customer this request is billable to. PER REQUEST. A hardcoded value makes per-customer cost attribution useless, thread it from request context. Required when attribution enforcement is enabled.","schema":{"type":"string"}},{"name":"x-spendline-tags","in":"header","required":false,"description":"JSON object that must include a cost-centre key: one of cost_center, costCenter, department, or team. Example: {\"team\":\"support\",\"env\":\"prod\"} Per call site or per request. Required when attribution enforcement is enabled.","schema":{"type":"string"}},{"name":"x-workflow-id","in":"header","required":false,"description":"Groups a multi-step run.","schema":{"type":"string"}},{"name":"x-step-name","in":"header","required":false,"description":"Labels a step within a workflow.","schema":{"type":"string"}},{"name":"x-source-route","in":"header","required":false,"description":"The application route that triggered the call.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["model","messages"],"properties":{"model":{"type":"string","description":"Selects the provider. See /agents/providers.md."},"messages":{"type":"array","items":{"type":"object"}},"stream":{"type":"boolean"},"max_tokens":{"type":"integer","description":"Also used to project pre-call cost for budget enforcement. A realistic value makes enforcement more accurate."}},"additionalProperties":true}}}},"responses":{"200":{"description":"Provider response, passed through unchanged. The call is recorded in the ledger."},"400":{"description":"missing_required_attribution, a required attribution header is absent while enforcement is on.","content":{"application/json":{"schema":{"type":"object","properties":{"error_type":{"type":"string","example":"missing_required_attribution"},"missing_fields":{"type":"array","items":{"type":"string"}}}}}}},"401":{"description":"The Spendline key is missing or invalid. Do NOT fall back to the provider."},"402":{"description":"A budget blocked this call BEFORE any spend occurred. Intended behaviour. Surface it; never retry against the provider directly."},"403":{"description":"A policy or governance rule blocked the call. Never route around it."},"429":{"description":"Rate limited. Back off and retry."},"502":{"description":"Upstream provider error, or Spendline could not reach it. This is the only class of failure where retrying directly against the provider is correct."}}}},"/v1/messages":{"post":{"tags":["proxy"],"operationId":"proxyAnthropicMessages","summary":"Anthropic messages, governed and recorded","description":"Set the Anthropic SDK baseURL to https://www.spendline.ai. WITHOUT /v1. The Anthropic SDK appends /v1/messages itself. This asymmetry with the OpenAI SDK is real; do not \"fix\" it.\n\nStreaming is supported.","parameters":[{"name":"x-api-key","in":"header","required":true,"description":"The application's OWN Anthropic key. Forwarded upstream. The SPENDLINE key must NOT go here, put it in x-spendline-key.","schema":{"type":"string"}},{"name":"anthropic-version","in":"header","required":true,"schema":{"type":"string","example":"2023-06-01"}},{"name":"x-agent-id","in":"header","required":false,"description":"Stable name of the bot/agent/feature making the call, e.g. support-bot. One per agent. Not per request. Required when attribution enforcement is enabled.","schema":{"type":"string"}},{"name":"x-customer-id","in":"header","required":false,"description":"The end customer this request is billable to. PER REQUEST. A hardcoded value makes per-customer cost attribution useless, thread it from request context. Required when attribution enforcement is enabled.","schema":{"type":"string"}},{"name":"x-spendline-tags","in":"header","required":false,"description":"JSON object that must include a cost-centre key: one of cost_center, costCenter, department, or team. Example: {\"team\":\"support\",\"env\":\"prod\"} Per call site or per request. Required when attribution enforcement is enabled.","schema":{"type":"string"}},{"name":"x-workflow-id","in":"header","required":false,"description":"Groups a multi-step run.","schema":{"type":"string"}},{"name":"x-step-name","in":"header","required":false,"description":"Labels a step within a workflow.","schema":{"type":"string"}},{"name":"x-source-route","in":"header","required":false,"description":"The application route that triggered the call.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["model","max_tokens","messages"],"properties":{"model":{"type":"string","example":"claude-sonnet-5"},"max_tokens":{"type":"integer"},"messages":{"type":"array","items":{"type":"object"}},"stream":{"type":"boolean"}},"additionalProperties":true}}}},"responses":{"200":{"description":"Anthropic response, passed through unchanged. The call is recorded."},"400":{"description":"missing_required_attribution, a required attribution header is absent while enforcement is on.","content":{"application/json":{"schema":{"type":"object","properties":{"error_type":{"type":"string","example":"missing_required_attribution"},"missing_fields":{"type":"array","items":{"type":"string"}}}}}}},"401":{"description":"The Spendline key is missing or invalid. Do NOT fall back to the provider."},"402":{"description":"A budget blocked this call BEFORE any spend occurred. Intended behaviour. Surface it; never retry against the provider directly."},"403":{"description":"A policy or governance rule blocked the call. Never route around it."},"429":{"description":"Rate limited. Back off and retry."},"502":{"description":"Upstream provider error, or Spendline could not reach it. This is the only class of failure where retrying directly against the provider is correct."}}}},"/v1/responses":{"post":{"tags":["proxy"],"operationId":"proxyOpenAiResponses","summary":"OpenAI Responses API, governed and recorded (non-streaming only)","description":"Same base URL as chat-completions.\n\n**stream: true is rejected with a clear error. Non-streaming only.**\n\nOpenAI models only. Built-in tools such as web_search work and their cost is included in recorded spend.","parameters":[{"name":"Authorization","in":"header","required":true,"description":"The application's OWN OpenAI key, as `Bearer <key>`.","schema":{"type":"string"}},{"name":"x-agent-id","in":"header","required":false,"description":"Stable name of the bot/agent/feature making the call, e.g. support-bot. One per agent. Not per request. Required when attribution enforcement is enabled.","schema":{"type":"string"}},{"name":"x-customer-id","in":"header","required":false,"description":"The end customer this request is billable to. PER REQUEST. A hardcoded value makes per-customer cost attribution useless, thread it from request context. Required when attribution enforcement is enabled.","schema":{"type":"string"}},{"name":"x-spendline-tags","in":"header","required":false,"description":"JSON object that must include a cost-centre key: one of cost_center, costCenter, department, or team. Example: {\"team\":\"support\",\"env\":\"prod\"} Per call site or per request. Required when attribution enforcement is enabled.","schema":{"type":"string"}},{"name":"x-workflow-id","in":"header","required":false,"description":"Groups a multi-step run.","schema":{"type":"string"}},{"name":"x-step-name","in":"header","required":false,"description":"Labels a step within a workflow.","schema":{"type":"string"}},{"name":"x-source-route","in":"header","required":false,"description":"The application route that triggered the call.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["model"],"properties":{"model":{"type":"string"},"input":{},"tools":{"type":"array","items":{"type":"object"},"description":"Built-in tools such as web_search work; their cost is included in recorded spend."},"stream":{"type":"boolean","description":"Must be false or omitted. true is rejected."}},"additionalProperties":true}}}},"responses":{"200":{"description":"OpenAI Responses payload, passed through unchanged."},"400":{"description":"missing_required_attribution, a required attribution header is absent while enforcement is on.","content":{"application/json":{"schema":{"type":"object","properties":{"error_type":{"type":"string","example":"missing_required_attribution"},"missing_fields":{"type":"array","items":{"type":"string"}}}}}}},"401":{"description":"The Spendline key is missing or invalid. Do NOT fall back to the provider."},"402":{"description":"A budget blocked this call BEFORE any spend occurred. Intended behaviour. Surface it; never retry against the provider directly."},"403":{"description":"A policy or governance rule blocked the call. Never route around it."},"429":{"description":"Rate limited. Back off and retry."},"502":{"description":"Upstream provider error, or Spendline could not reach it. This is the only class of failure where retrying directly against the provider is correct."}}}},"/api/calls":{"get":{"tags":["verification"],"operationId":"listCalls","summary":"List recorded calls, use this to prove attribution is correct","description":"Paginated with `page` (fixed page size). There is no `limit` parameter. After a smoke test, confirm the newest row carries the correct customer_id and a non-zero cost_usd, a 200 from the proxy does not by itself prove attribution.","parameters":[{"name":"page","in":"query","schema":{"type":"integer","minimum":1,"default":1}},{"name":"customer","in":"query","schema":{"type":"string"}},{"name":"agent","in":"query","schema":{"type":"string"}},{"name":"model","in":"query","schema":{"type":"string"}},{"name":"status","in":"query","schema":{"type":"string","enum":["budget","error","success"]}},{"name":"timeRange","in":"query","schema":{"type":"string"}},{"name":"sourceRoute","in":"query","schema":{"type":"string"}},{"name":"success","in":"query","schema":{"type":"boolean"}},{"name":"search","in":"query","schema":{"type":"string"}},{"name":"tags","in":"query","description":"JSON object; every key/value must match.","schema":{"type":"string"}}],"responses":{"200":{"description":"A page of recorded calls."},"401":{"description":"Unauthorized."}}}},"/api/stats/summary":{"get":{"tags":["verification"],"operationId":"statsSummary","summary":"Spend summary for the last 30 days","responses":{"200":{"description":"Total spend, call count, average cost, cost over time, by model, by customer/agent."},"401":{"description":"Unauthorized."}}}},"/api/budgets":{"get":{"tags":["budgets"],"operationId":"listBudgets","summary":"List hierarchical budgets","responses":{"200":{"description":"Budgets for this account."},"401":{"description":"Unauthorized."}}},"post":{"tags":["budgets"],"operationId":"createBudget","summary":"Create a hierarchical budget (owner or admin only)","description":"Requires OWNER or ADMIN authority. A scoped child key cannot do this and receives 403 permission_denied. To have a budget created from an agent-initiated flow, propose it in the onboarding request so a human approves the exact numbers.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Budget"}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Budget"}}}},"400":{"description":"Invalid scope_type, missing scope_id, or negative limit."},"403":{"description":"Insufficient authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PermissionDenied"}}}},"409":{"description":"A budget already exists for this scope. Use PUT /api/budgets/{id}."}}}},"/api/budgets/summary/all":{"get":{"tags":["budgets"],"operationId":"budgetSummaryAll","summary":"Every budget scope with current spend against its limit","responses":{"200":{"description":"Spend against limit per scope."},"401":{"description":"Unauthorized."}}}},"/api/budgets/scopes":{"get":{"tags":["budgets"],"operationId":"listBudgetScopes","summary":"Agents, customers and teams with traffic this month","description":"The correct way to discover scope_ids worth budgeting, instead of guessing identifiers. Also the fastest attribution check: if `customers` holds one entry after calls for several customers, x-customer-id is pinned in default headers instead of per request.","responses":{"200":{"description":"Discovered scope identifiers.","content":{"application/json":{"schema":{"type":"object","properties":{"agents":{"type":"array","items":{"type":"string"}},"customers":{"type":"array","items":{"type":"string"}},"teams":{"type":"array","items":{"type":"string"}}}}}}},"401":{"description":"Unauthorized."}}}},"/api/budgets/overrides":{"get":{"tags":["budgets"],"operationId":"listBudgetOverrides","summary":"List budget override requests","parameters":[{"name":"status","in":"query","schema":{"type":"string","example":"pending"}}],"responses":{"200":{"description":"Override requests."},"401":{"description":"Unauthorized."}}},"post":{"tags":["budgets"],"operationId":"requestBudgetOverride","summary":"Request an override for a blocked call","description":"The correct response to a wrongly-blocked 402: it leaves an approver on the record. Approving an override is owner-authorized and is NOT exposed to agents.","responses":{"201":{"description":"Override requested; awaiting owner approval."},"401":{"description":"Unauthorized."}}}},"/api/policies":{"get":{"tags":["policies"],"operationId":"listPolicies","summary":"List policies","responses":{"200":{"description":"Policies for this account."},"401":{"description":"Unauthorized."}}},"post":{"tags":["policies"],"operationId":"createPolicy","summary":"Create a policy (owner or admin only)","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Policy"}}}},"responses":{"201":{"description":"Created."},"400":{"description":"Invalid type, match_mode, enforcement, scope, or a missing token value."},"403":{"description":"Insufficient authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PermissionDenied"}}}}}}},"/api/policies/blocked-events":{"get":{"tags":["policies"],"operationId":"listBlockedEvents","summary":"Calls a policy has blocked","responses":{"200":{"description":"Blocked events."},"401":{"description":"Unauthorized."}}}},"/api/reroutes":{"get":{"tags":["rerouting"],"operationId":"listReroutes","summary":"List reroute rules","responses":{"200":{"description":"Reroute rules."},"401":{"description":"Unauthorized."}}},"post":{"tags":["rerouting"],"operationId":"createReroute","summary":"Create a reroute rule (owner or admin only)","description":"A cross-provider reroute requires an ACTIVE stored provider key for the target, which only a human can add. Without one this returns 400 missing_provider_key.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Reroute"}}}},"responses":{"201":{"description":"Created."},"400":{"description":"Identical models, an unroutable target, or missing_provider_key for a cross-provider target.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Insufficient authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PermissionDenied"}}}}}}},"/api/reroutes/savings":{"get":{"tags":["rerouting"],"operationId":"rerouteSavings","summary":"Realized savings from reroute rules","responses":{"200":{"description":"Savings summary."},"401":{"description":"Unauthorized."}}}},"/api/optimizer/summary":{"get":{"tags":["rerouting"],"operationId":"optimizerSummary","summary":"Ranked cheaper-model suggestions from real recorded traffic","responses":{"200":{"description":"Optimizer suggestions."},"401":{"description":"Unauthorized."}}}},"/api/agent-onboarding/request":{"post":{"tags":["onboarding"],"operationId":"startAgentOnboarding","summary":"Start agent-initiated, human-authorized provisioning","description":"PUBLIC: no credential required, because the caller has none yet.\n\nIssues NO credential. It emails the named human, who must approve in a browser.\nShaped after RFC 8628 (OAuth 2.0 Device Authorization Grant).\n\nAn agent cannot create an account, cannot choose a password, and cannot approve\nits own request.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["human_email"],"properties":{"human_email":{"type":"string","format":"email","description":"The human who owns this application. They receive the approval email."},"agent_name":{"type":"string","description":"How to identify you to the human."},"intended_use":{"type":"string","description":"Plain sentence shown to the human."},"proposed_budget":{"type":"object","description":"Optional initial budget to PROPOSE. The human sees the exact numbers and approves or skips it; if approved it is created under their authority.","properties":{"scope_type":{"type":"string","enum":["org","team","agent","customer"]},"scope_id":{"type":"string"},"monthly_limit_usd":{"type":"number"},"strict_mode":{"type":"boolean","default":true}}}}}}}},"responses":{"201":{"description":"Request created. Poll for the outcome.","content":{"application/json":{"schema":{"type":"object","properties":{"request_id":{"type":"string","format":"uuid"},"device_code":{"type":"string","description":"Your poll credential. Shown once."},"user_code":{"type":"string","description":"What the human sees. Not a credential."},"verification_uri":{"type":"string","format":"uri"},"verification_uri_complete":{"type":"string","format":"uri"},"interval":{"type":"integer","description":"Seconds between polls."},"expires_in":{"type":"integer"},"status":{"type":"string","example":"pending_human_authorization"},"human_action_required":{"type":"string"}}}}}},"400":{"description":"human_email missing/invalid, or a malformed proposed_budget."},"429":{"description":"Rate limited."},"503":{"description":"This deployment cannot send the authorization email, so the flow cannot complete. Fall back to asking the human to create a key in the dashboard."}}}},"/api/agent-onboarding/poll":{"post":{"tags":["onboarding"],"operationId":"pollAgentOnboarding","summary":"Poll for approval and collect the scoped key exactly once","description":"On the first poll after approval this returns a SCOPED CHILD key: proxy + reads only. It is never the account root key, and it is shown exactly once. Subsequent polls return status \"consumed\".","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["request_id","device_code"],"properties":{"request_id":{"type":"string","format":"uuid"},"device_code":{"type":"string"}}}}}},"responses":{"200":{"description":"Current state. status is one of pending_human_authorization, approved, denied, expired, consumed. api_key is present only on the single approved response.","content":{"application/json":{"schema":{"type":"object","properties":{"request_id":{"type":"string"},"status":{"type":"string","enum":["pending_human_authorization","approved","denied","expired","consumed"]},"api_key":{"type":"string","description":"Present once, on approval. Store as SPENDLINE_API_KEY."},"key_type":{"type":"string","example":"scoped_child"},"scope":{"type":"object"},"initial_budget":{"type":"object"}}}}}},"400":{"description":"request_id or device_code missing."},"404":{"description":"Unknown request, or the device_code does not match."}}}},"/agents/capabilities.json":{"get":{"tags":["discovery"],"operationId":"getCapabilities","summary":"Providers, request shapes, attribution headers and the response contract","description":"PUBLIC, no auth. Resolve routing and header requirements from here at runtime rather than hardcoding them from prose.","security":[],"responses":{"200":{"description":"Machine-readable capability description."}}}},"/agents/when-to-use.json":{"get":{"tags":["discovery"],"operationId":"getWhenToUse","summary":"Intent → capability map, plus when to choose a different tool","security":[],"responses":{"200":{"description":"Selection guidance for agents."}}}}},"x-spendline":{"positioning":"Financial control, not engineering observability. Spendline blocks spend before it occurs (HTTP 402), keeps an append-only ledger suitable for finance, and closes the month like any other COGS line.","providers":["openai","anthropic","google","xai","mistral","deepseek","qwen","together","fireworks","groq"],"response_contract":[{"status":200,"meaning":"Forwarded and recorded.","agent_action":"Done."},{"status":400,"error_type":"missing_required_attribution","meaning":"Attribution enforcement is on and a required header is missing.","agent_action":"Add x-agent-id, x-customer-id and x-spendline-tags (with a cost-centre key)."},{"status":401,"meaning":"The Spendline key is missing or invalid.","agent_action":"Check SPENDLINE_API_KEY. Do NOT fall back to the provider."},{"status":402,"meaning":"A budget blocked this call before any spend occurred.","agent_action":"This is correct, intended behaviour. Surface it as an error. NEVER retry against the provider directly, that defeats the entire product."},{"status":403,"meaning":"A policy or governance rule blocked the call.","agent_action":"Surface it. NEVER route around it."},{"status":429,"meaning":"Rate limited.","agent_action":"Back off and retry."},{"status":"5xx / network error / timeout","meaning":"Spendline is unreachable or failed.","agent_action":"Retry once directly against the provider so the application stays up. This is the ONLY case where bypassing Spendline is correct."}],"human_authorization_required":["billing and payment methods","storing provider API keys","raising or deleting a budget","approving a budget override","closing, reopening or reconciling a month","creating an account","issuing an API key to an agent"],"documentation_index":"https://www.spendline.ai/agents/index.md"}}