# Spendline integration — instructions for your coding agent

> **For the human reading this:** give this file to your coding agent (Claude Code,
> Cursor, Copilot, etc.) with a message like:
>
> "Read https://www.spendline.ai/integrate.md and integrate this app with Spendline.
> My Spendline URL is `https://www.spendline.ai` and my Spendline API key is in the
> environment as `SPENDLINE_API_KEY`."
>
> That's all you need to do. Everything below is written for the agent.
>
> **Came from the dashboard's Connect page?** Your agent should follow
> <https://www.spendline.ai/agents/connect.md> instead of starting here. That
> flow analyses the repository in your environment, proposes how spend should
> be attributed, waits for you to confirm in the dashboard, and only then
> applies the phases below. This file is the reference it uses for the edits.
> Two values it needs from you: your **Spendline URL** (`https://www.spendline.ai`
> unless you were given a pilot/self-hosted URL) and your **Spendline API key**
> (Dashboard → API Keys). Never paste the key into chat — put it in your `.env`.

---

> **Related, and worth fetching first if you have not already.**
> <https://www.spendline.ai/agents/index.md> is the full agent documentation
> index, covering per-provider guides, attribution, budgets, policies,
> verification and troubleshooting.
> <https://www.spendline.ai/agents/when-to-use.md> tells you whether Spendline
> is even the right tool for this requirement, and names the alternative when
> it is not. <https://www.spendline.ai/agents/onboarding.md> covers what to do
> when the human has no Spendline account yet.

---

You are integrating this application with **Spendline**, an AI spend-governance
proxy. Spendline sits between this app and its AI providers (OpenAI, Anthropic,
Google, xAI, Mistral, and OpenAI-compatible hosts like DeepSeek, Qwen, Together AI,
Fireworks AI, and Groq). The app keeps its existing provider API keys; you repoint
its AI clients at the proxy and add attribution headers so each supported call is
budgeted, policy-checked, and recorded against the right customer and team. Calls
sent directly to a provider are outside Spendline's controls and records.

Spendline has two deployments and this file covers one of them. **Phase 0 below is how
you tell them apart, and it is not optional** — picking the wrong one is not a style
choice, it changes whether prompts pass through Spendline.

Real apps call several providers from several places. Do not assume there is one
client in one file. Work through the phases below **in order**, and do not skip
Phase 1.

`<SPENDLINE_URL>` below means the Spendline URL the human gave you
(default `https://www.spendline.ai`).

## Phase 0 — Confirm this is the right integration (do this first)

**This document describes the HOSTED PROXY.** In it, the app's requests travel to
Spendline, which forwards them to the provider. Prompts and completions transit our
infrastructure. That is the correct integration for most teams and it is what the rest
of this file sets up.

Spendline also has an **in-network deployment**, where Spendline runs inside the
customer's own network as a library. Spendline never sees the prompts: the library
enforces budgets inside the app, the app calls the provider directly, and only a
metadata-only receipt is sent to Spendline. That integration is the opposite shape:
**nothing is repointed** — no `baseURL` change, no attribution headers on the wire —
and following this document instead would route prompts through Spendline, which is
exactly what that deployment was chosen to avoid.

**STOP and ask the human before changing any code if any of these are true:**

- they said "in-network", "in our own network", "on-premise", "self-hosted", or "the
  prompts must not leave our infrastructure",
- they are in a regulated environment (a bank, an insurer, a government entity) and
  data location is part of why they bought this,
- they gave you a `.tgz` file or a `SPENDLINE_KEY` (note: not `SPENDLINE_API_KEY`)
  rather than a URL to point at.

In that case this is the wrong document. The in-network deployment is delivered with
its own install instructions and is not a change you can make from here. Say so and
stop; do not improvise a hybrid.

If none of that applies, continue.

## Phase 1 — Audit (before changing anything)

Find **every** place this app talks to an AI provider:

- SDK client constructions: `new OpenAI(`, `OpenAI(` (Python), `new Anthropic(`,
  `Anthropic(` (Python), `AsyncOpenAI`, `AsyncAnthropic`, wrappers from LangChain /
  LlamaIndex / Vercel AI SDK that accept a `baseURL`/`base_url`.
- Raw HTTP calls to provider hosts: `api.openai.com`, `api.anthropic.com`,
  `generativelanguage.googleapis.com`, `api.x.ai`, `api.mistral.ai`,
  `api.deepseek.com`, `api.together.xyz`, `api.fireworks.ai`, `api.groq.com`.
- Env vars and config files that hold provider base URLs.

Produce an inventory table before editing: file, line, provider, API shape
(chat-completions / messages / responses / embeddings / other), streaming or not.
Show it to the human in your final report.

## Phase 2 — Classify each call site

Spendline accepts exactly **three request shapes**:

| Shape | Endpoint | Notes |
|---|---|---|
| OpenAI chat-completions | `<SPENDLINE_URL>/v1/chat/completions` | All OpenAI-compatible providers. Streaming supported. The **model name** picks the provider (`gpt-*` → OpenAI, `gemini-*` → Google, `grok-*` → xAI, `mistral-*` → Mistral, etc.) |
| Anthropic messages | `<SPENDLINE_URL>/v1/messages` | Claude models. Streaming supported. |
| OpenAI Responses API | `<SPENDLINE_URL>/v1/responses` | OpenAI models only. **Non-streaming only** — `stream: true` is rejected with a clear error. Built-in tools like `web_search` work, and their cost is included in the recorded spend. |

**Leave unsupported shapes alone**: embeddings, vector/knowledge-base search,
image/audio generation, and Google's native `generateContent` keep pointing
directly at their provider. Mark them "left as-is" in your report — do not route
them through Spendline.

## Phase 3 — Repoint the supported clients

- **OpenAI SDK** (and any OpenAI-compatible client): set `baseURL` / `base_url` to
  `<SPENDLINE_URL>/v1`. The SDK appends `/chat/completions` (and `/responses`) itself.
- **Anthropic SDK**: set `baseURL` to `<SPENDLINE_URL>` — **no `/v1`**. The
  Anthropic SDK appends `/v1/messages` itself. This asymmetry between the two SDKs
  is real; do not "fix" one to match the other.
- **Raw HTTP**: replace the provider host with the matching Spendline endpoint
  from the table above.
- **Keep the app's existing provider API keys exactly where they are** (OpenAI's
  `Authorization: Bearer`, Anthropic's `x-api-key`). Spendline forwards them to the
  provider; it does not replace them.
- If clients are constructed in many places, prefer extracting one shared
  factory/module so the Spendline config lives in one spot — but only if that
  refactor is safe and small in this codebase.

**Framework wrappers** (same rules apply; only the config surface differs):

- **LangChain**: `ChatOpenAI` takes `base_url`/`baseURL` and
  `default_headers`/`defaultHeaders`; `ChatAnthropic` takes the Anthropic base URL
  (no `/v1`) via its client options. Set the Spendline headers there.
- **Vercel AI SDK**: `createOpenAI({ baseURL: '<SPENDLINE_URL>/v1', headers: {...} })`
  and `createAnthropic({ baseURL: '<SPENDLINE_URL>', headers: {...} })`.
- **Other wrappers** (LlamaIndex, LiteLLM, etc.): find where the wrapper exposes
  the underlying client's base URL and default headers and apply the same values.
  If a wrapper offers no way to set headers, call that out in your report instead
  of forcing it.
- In frameworks, `x-customer-id` still must vary per request — pass headers at
  call time where the framework supports it, or construct the model client where
  the customer is known.

## Phase 4 — Add attribution headers to every routed call

Send these on every request through Spendline. Headers that never change for this
app (`x-spendline-key`, `x-agent-id`, `x-spendline-tags`) can go in the SDK's
`defaultHeaders` / `default_headers`. Headers that change per request
(`x-customer-id`, `Idempotency-Key`) must be passed **per request**, never in a
shared client's defaults:

- `x-spendline-key` — the Spendline API key, read from env (`SPENDLINE_API_KEY`).
  **Never** put it in `x-api-key`: Anthropic uses `x-api-key` for the Claude
  provider key, so the Spendline key would be sent to Anthropic and rejected.
- `x-agent-id` — a stable name for the bot/agent/feature making the call
  (e.g. `support-bot`, `doc-summarizer`). One per agent, not per request.
- `x-customer-id` — the end-user/customer who triggered **this** request. Set it
  **per request**, not per process — a hardcoded value makes per-customer cost
  tracking useless. If the call site doesn't know the customer, thread it through
  from the request context; ask the human if there is genuinely no customer concept.
- `x-spendline-tags` — a JSON object that must include a cost-centre key: one of
  `cost_center`, `costCenter`, `department`, or `team`.
  Example: `{"team":"support","env":"prod"}`.

- `Idempotency-Key` — **required on every call.** One value (a UUID) per logical
  request, generated **before the first attempt** and reused unchanged on every
  retry of that request. This is how Spendline makes sure the work is bought
  from the provider and billed exactly once: a repeat of a key that already
  completed is answered `409 duplicate_request` without contacting the provider
  again, so a retry after a timeout can never buy the answer twice. Generate it
  where the request is created, **not** inside the retry loop — a fresh value per
  attempt makes every retry a separate purchase, which is the whole failure this
  header prevents.

Optional, for richer dashboards: `x-workflow-id` (groups a multi-step run),
`x-step-name` (labels a step), `x-source-route` (the app route that triggered the
call).

Calls missing the required fields are rejected with HTTP 400
`missing_required_attribution` when attribution enforcement is on — send them from
day one. A call with no `Idempotency-Key` is rejected with HTTP 400
`idempotency_key_required` before anything reaches the provider.

## Phase 5 — Resilience and config

- Put the Spendline base URL and key in env config (`SPENDLINE_URL`,
  `SPENDLINE_API_KEY`) behind **one on/off flag** (e.g. `SPENDLINE_ENABLED`) so the
  proxy can be disabled without a code change.
- On a **network error, timeout, or 5xx** from Spendline, retry once directly
  against the provider so the app stays up. Reuse the SAME `Idempotency-Key` on
  the retry.
- A `409 requires_reconciliation` means an earlier attempt under that key may
  have reached the provider and its outcome is unknown. **Do not retry it under a
  new key** — that risks paying twice. Surface it; a finance operator resolves it
  in the dashboard.
- **Never** fall back on `401`, `403`, or `402`. `402` is a deliberate budget
  block and `403` a policy block — routing around them defeats the point.
  Surface these to the app as a clear error instead.

## Phase 6 — Verify

For each provider the app uses, make one real call through Spendline — one
streaming and one non-streaming where the app uses both. (Responses API:
non-streaming only.) Then check the results:

| Result | Meaning | Fix |
|---|---|---|
| 200 + normal response | Connected; call appears in the Spendline dashboard within seconds | Done |
| 401 | Spendline key rejected | Fresh key from Dashboard → API Keys into `SPENDLINE_API_KEY` |
| 400 `missing_required_attribution` | Required headers missing | Send `x-agent-id`, `x-customer-id`, and `x-spendline-tags` with a cost-centre key |
| 400 `idempotency_key_required` | No `Idempotency-Key` was sent | Generate one UUID per logical request before the first attempt; reuse it on retries |
| 409 `duplicate_request` | This key already completed | Working as intended — use the recorded result; do not re-send under a new key |
| 409 `requires_reconciliation` | An earlier attempt may have reached the provider; outcome unknown | Do not retry under a new key — a finance operator resolves it in the dashboard |
| 402 | A budget blocked the call | Expected behavior — check Dashboard → Budgets |
| 403 | A policy/governance rule blocked it | Check Dashboard → Policies and Governance |
| Other 4xx/5xx | Usually a wrong provider key or unknown model name | Check the provider key and model |

Ask the human to confirm the calls appear in their Spendline dashboard.

## Phase 7 — Report

Finish with a summary for the human: the Phase 1 inventory table with a status
column — **repointed**, **left as-is (unsupported shape)**, or **needs a
decision** — plus the env vars you added and the verification results per
provider.

## If you get stuck

If you cannot complete the integration or a verification call keeps failing after
applying the fixes above, don't loop — produce a **diagnostic block** for the
human containing: the Phase 1 inventory table, the exact failing request (URL,
headers **with all key values redacted**, model) and the full response body,
and the SDK/framework names and versions involved. Tell the human to email it to
**fida@spendline.ai** — that block contains everything needed to answer in one
reply.
