Connect one supported AI workflow.
Spendline sits between your application and supported AI providers. For one workflow, your engineer changes the endpoint, adds attribution labels to each request and handles Spendline’s control responses. Coding tools can read the same instructions at /integrate.md.
Prerequisites
- A Spendline API key, kept on your server as
SPENDLINE_API_KEY. Pilot accounts are set up with you; existing users find keys under Dashboard → API Keys. Never put it in browser code or paste it into a chat. - Your own provider keys stay where they are. Spendline forwards them to the provider; it does not replace them.
- One workflow that uses a supported route below, and a test environment to verify it in.
- A place in your server code that knows which customer a request is for, such as the authenticated session or the job payload.
Supported routes
| Request shape | Providers | SDK base URL | Streaming |
|---|---|---|---|
/v1/chat/completions | OpenAI, Google Gemini, xAI, Mistral, DeepSeek, Qwen, Together AI, Fireworks AI, Groq | https://www.spendline.ai/v1 | Supported |
/v1/messages | Anthropic | https://www.spendline.ai | Supported |
/v1/responses | OpenAI | https://www.spendline.ai/v1 | Non-streaming only |
The OpenAI SDK base URL ends in /v1; the Anthropic SDK base URL does not, because that SDK adds /v1/messages itself. On chat completions the model name selects the provider. Google Gemini is reached through its OpenAI-compatible chat-completions endpoint.
Embeddings, images, audio and speech, vector or knowledge-base search, and Google’s native generateContent API are not supported. Leave those calls pointed at the provider. Requests sent directly to a provider, including any direct-provider fallback, and unsupported API shapes are outside Spendline’s controls and records.
Examples
Stable settings go on the client: the base URL, the Spendline key and the workflow labels. The customer ID and the Idempotency-Key go on each request. Do not put a customer ID in a shared client’s default headers: every call would be recorded against that one customer. Values come from environment variables; none of these are real keys.
OpenAI SDK (Node.js), chat completions
import OpenAI from "openai";
import { randomUUID } from "node:crypto";
// One client per process: stable settings only.
const openai = new OpenAI({
baseURL: "https://www.spendline.ai/v1",
apiKey: process.env.OPENAI_API_KEY, // your own OpenAI key, forwarded to OpenAI
defaultHeaders: {
"x-spendline-key": process.env.SPENDLINE_API_KEY,
"x-agent-id": "support-bot",
"x-spendline-tags": JSON.stringify({ team: "support" }),
},
});
// Per request: the customer comes from your authenticated server context.
export async function draftReply(account, messages) {
return openai.chat.completions.create(
{ model: "gpt-4o-mini", max_tokens: 500, messages },
{
headers: {
"x-customer-id": account.id,
"Idempotency-Key": randomUUID(), // reused by the SDK's own retries
},
},
);
}Anthropic SDK (Node.js), messages
Anthropic uses x-api-key for your Anthropic key. Send the Spendline key in x-spendline-key only. A Spendline key placed in x-api-key is refused with a 401 and is never forwarded to Anthropic.
import Anthropic from "@anthropic-ai/sdk";
import { randomUUID } from "node:crypto";
const anthropic = new Anthropic({
baseURL: "https://www.spendline.ai", // no /v1: the SDK adds /v1/messages
apiKey: process.env.ANTHROPIC_API_KEY, // your Anthropic key, forwarded to Anthropic
defaultHeaders: {
"x-spendline-key": process.env.SPENDLINE_API_KEY,
"x-agent-id": "support-bot",
"x-spendline-tags": JSON.stringify({ team: "support" }),
},
});
export async function summarise(account, text) {
return anthropic.messages.create(
{ model: "claude-haiku-4-5", max_tokens: 500, messages: [{ role: "user", content: text }] },
{
headers: {
"x-customer-id": account.id,
"Idempotency-Key": randomUUID(),
},
},
);
}curl
curl https://www.spendline.ai/v1/chat/completions \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "x-spendline-key: $SPENDLINE_API_KEY" \
-H "x-agent-id: smoke-test" \
-H "x-customer-id: test-customer-1" \
-H 'x-spendline-tags: {"team":"platform"}' \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-4o-mini","max_tokens":50,"messages":[{"role":"user","content":"hello"}]}'Attribution
Your application supplies the customer and workflow context. Routing alone does not discover the customer, and untagged traffic leaves gaps in customer reporting.
| Header | Set it | Value |
|---|---|---|
x-spendline-key | Every request | Your Spendline API key, from the environment. |
Idempotency-Key | Every request | One value per logical request, created before the first attempt and reused on its retries. |
x-customer-id | Per request | The customer this request is for, from trusted server context. |
x-agent-id | Per workflow | A stable name for the feature or agent, such as support-bot. |
x-spendline-tags | Per call site | A JSON object with a cost-centre key: cost_center, costCenter, department or team. |
x-workflow-id, x-step-name, x-source-route | Optional | Group a multi-step run, label a step, name the app route. |
Set customer IDs on your server from authenticated request context. Spendline records the supplied customer ID and server time. Spendline does not check that the ID belongs to the caller, so take it from your authenticated session or job, never from a value the browser sends.
Spendline refuses a request without x-spendline-key or Idempotency-Key. The customer, agent and tags headers become mandatory only if attribution enforcement is switched on for your account; a request missing one is then refused with 400 missing_required_attribution before it reaches the provider.
What incomplete attribution looks like
- A request without a customer ID is still forwarded and its cost recorded, but under no customer: it shows as
(none)in the customer breakdown and cannot be compared with any customer’s revenue. - A customer ID pinned in a shared client’s default headers puts every request on one customer.
- Requests that go directly to a provider do not appear in Spendline at all.
Budget responses
Spendline checks configured budgets before forwarding supported requests. Strict budgets reject requests whose estimated cost would exceed a limit. Actual response cost can differ from the estimate.
The estimate uses the request’s output limit (max_tokens or max_output_tokens), or a 1,000-token default when none is set, so a realistic limit makes the check more accurate. A response that has started is not cut off, so its final cost can pass the limit.
| Status | Meaning | What your code does |
|---|---|---|
402 | A budget refused the request before it reached the provider. No provider charge. | Show a clear error to the user or job. Do not retry against the provider. |
403 | A policy refused the request, or the credential lacks permission. | Show the error. Do not route around it. |
503 | Spendline could not confirm spend for this request, so it refused rather than let unrecorded spend through. | See failure behavior below. |
402 and 403 are intentional control responses. Code that bypasses them by calling the provider directly defeats the budget and leaves the cost unrecorded.
Failure behavior
401: the Spendline key is missing or wrong. Fix the key; do not fall back.400 missing_required_attributionor400 idempotency_key_required: add the missing headers. Nothing was sent to the provider.409 duplicate_request: thatIdempotency-Keyalready completed. Use the earlier result; do not resend under a new key. The OpenAI and Anthropic SDKs retry a 409 twice by default, so you may see this after three attempts; that is harmless because the same key is reused and nothing is bought twice.409 requires_reconciliation: an earlier attempt may have reached the provider. Do not retry under a new key; it is resolved in the dashboard.429: back off and retry later.
For a network error, timeout or 5xx from Spendline, the coding-tool guide suggests one retry directly against the provider so your application stays up, reusing the same Idempotency-Key.
A direct-provider fallback is outside Spendline: no budget check, no policy check and no record in Spendline’s ledger, so its cost does not appear in customer reporting. Decide per workflow whether to allow it or fail closed, and never use it for 401, 402 or 403.
Verification
Run these in a test environment, with a test customer ID.
- Send one request through the changed route with a test customer ID, for example
test-customer-1. Expect a normal response. - In the dashboard, find the call under that customer ID, with the workflow name you sent and its cost. If it shows under
(none), or every call shares one customer, recheck the attribution headers. - Ask an owner or admin to add a strict customer-scoped budget with a small limit for the test customer. Send requests until the next one’s estimate would pass the limit. Expect
402, no provider charge for that request, and your application showing the error without retrying the provider. - Remove the test budget afterwards.
Production calls, provider-key changes and budget changes keep their usual approvals. Do not change production budgets to run this check.
Rollback
Keep the Spendline base URL and key in configuration behind one switch, such as SPENDLINE_ENABLED. Turning it off points the client back at the provider’s own endpoint and stops sending Spendline headers, with no code change. Records already made stay in Spendline. Requests made while it is off are not budgeted or recorded.
Coding-agent prompt
Optional. If your engineer uses a coding agent, this prompt points it at the machine guide.
Show the coding-agent prompt
For Cursor, Claude Code, Codex or a similar agent. It reads the machine guide at /integrate.md, lists every place your app calls a model, and proposes the change in your own codebase. Review what it changes; never paste a key into the chat.
Before changing anything, read https://www.spendline.ai/integrate.md. Phase 0 of integrate.md first: this is the hosted proxy. If I have said in-network, on-premise, or that prompts must not pass through Spendline, or I gave you a .tgz or a SPENDLINE_KEY, STOP and tell me. Spendline never sees the prompts in that deployment and nothing gets repointed there. Otherwise integrate this app with Spendline, an AI spend-governance proxy, following that guide in order. My Spendline URL is https://www.spendline.ai. My Spendline API key belongs in .env as SPENDLINE_API_KEY. If it is not there, follow https://www.spendline.ai/agents/onboarding.md so I can approve access in my browser. If I do not have a Spendline account yet, tell me I need to create one first, through a pilot call booked at https://www.spendline.ai/book/ or a team invite, then approve; never create it for me, and never ask me to paste a key into chat. When you are done, make one cheap real call, check the response against the guide's Phase 6 table, and tell me to confirm the call in my Spendline dashboard.
Want help with the first workflow?
Bring one example of how your app calls AI. We will map the customer labels and agree what a pilot would test. See also our data posture.
Prefer email? fida@spendline.ai