How to Integrate an LLM API: A Developer's Guide
Integrating an LLM API means connecting your application's backend (or frontend, in some cases) to a hosted language model so it can generate text, hold conversations, or call tools on your behalf. Practically, that comes down to five things: getting an API key, sending correctly formatted HTTP requests, handling the response (including streaming), managing errors and rate limits, and tracking usage so you know what it's costing you.
This guide walks through that process end to end, with working code, so you can go from "we want AI features" to a shipped integration without getting stuck on the parts that documentation usually glosses over — auth headers, streaming parsing, and what happens when a request fails mid-response.
Step 1: Get API access and an API key
Every LLM provider issues an API key tied to an account. You'll set this as an environment variable — never hardcode it or commit it to version control. Most integrations start the same way:
export API_KEY="your-api-key-here"
If you're building on Claude specifically but don't have direct API billing set up (for example, you're using a Claude subscription rather than a metered API account), a service like SubToAPI issues you a standard sub_live_... key from your existing Claude access, so you can integrate the same way you would against any vendor API. Check /pricing if that's your situation — it doesn't change any of the integration steps below.
Step 2: Understand the request/response shape
Almost all modern LLM APIs use a "messages" format: a list of role-tagged turns (user, assistant, sometimes system) sent as JSON in a POST request. Here's the general shape:
curl https://api.example.com/v1/messages \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "your-model-name",
"max_tokens": 1024,
"messages": [
{"role": "user", "content": "Summarize this ticket in one sentence."}
]
}'
The response comes back as JSON with the generated text, a stop reason, and usage metadata (input/output token counts). That usage field matters — it's what you'll use later for cost tracking and billing your own customers if you're building a multi-tenant product. See /docs/messages for the exact request/response schema if you're integrating with SubToAPI.
Step 3: Add streaming for real-time output
Non-streaming requests wait for the full response before returning anything, which feels slow for anything longer than a sentence. Streaming sends the response in chunks as it's generated, using server-sent events:
const response = await fetch("https://api.subtoapi.app/v1/messages", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.SUBTOAPI_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
model: "claude-sonnet-4",
max_tokens: 1024,
stream: true,
messages: [{ role: "user", content: "Write a short changelog entry." }]
})
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
process.stdout.write(decoder.decode(value));
}
Your frontend then needs to handle partial chunks — appending text as it arrives rather than waiting for a full payload. This is the single most common thing teams get wrong on a first integration: they build the happy path for a complete JSON response and have to retrofit streaming later. Build it in from day one if your UI shows generated text directly to users. Full details are in /docs/streaming.
Step 4: Wire up tool use if you need it
If your LLM needs to call your own functions — look up an order, query a database, hit an internal API — you'll define tools as JSON schemas alongside your request. The model returns a structured "tool call" instead of plain text when it decides one is needed, your code executes it, and you send the result back in a follow-up message. This loop (request → tool call → your execution → response with result → final answer) is the core pattern behind most "AI agent" features. Get the schema right and this part is straightforward; see /docs/tools for a worked example with request/response pairs.
Step 5: Handle errors, retries, and rate limits
Production integrations fail in specific, predictable ways:
- 429 (rate limited): back off and retry with exponential delay, don't hammer the endpoint
- Timeouts on long generations: set a generous client timeout, and prefer streaming so partial output isn't lost
- Malformed or truncated responses: check the
stop_reasonfield — if it saysmax_tokens, your response was cut off, not broken - Auth errors: confirm your key is being sent as
Authorization: Bearer $KEYand hasn't been rotated
A minimal retry wrapper:
async function callWithRetry(fn, attempts = 3) {
for (let i = 0; i < attempts; i++) {
try {
return await fn();
} catch (err) {
if (i === attempts - 1) throw err;
await new Promise(r => setTimeout(r, 500 * 2 ** i));
}
}
}
Step 6: Track usage before it surprises you
Once the integration works, the next problem is cost visibility. Every request returns token counts — log them per user or per feature from the start, not after your first surprise invoice. If you have multiple developers or teams building against the same underlying model access, a dashboard that separates application keys per project (rather than one shared key everyone uses) makes this much easier to audit. SubToAPI's dashboard gives each application its own sub_live_... key with usage metadata and seat-based team access, which is useful once more than one person is shipping against the same account. You can start with the free trial at /signup and follow /docs/quickstart for a working first request in a few minutes.
A basic integration checklist
- API key stored as an environment variable, not in code
- Non-streaming request working end to end with error handling
- Streaming implemented if output is user-facing
- Retry logic for rate limits and transient failures
- Tool calling wired up if your feature needs function execution
- Usage/token logging in place before launch
- A staging key separate from production
FAQ
Do I need a different integration for every LLM provider?
The core pattern (messages array, POST request, JSON or streamed response) is similar across providers, but exact field names, headers, and tool-calling schemas differ. Check each provider's docs for the specifics, but the concepts you learn on one integration transfer directly to the next.
Should I integrate directly against the vendor or through a middle layer?
Direct integration works fine for a single provider and predictable usage. A middle layer (like SubToAPI for Claude access) helps when you need per-app API keys, team seats, and usage tracking without building that infrastructure yourself — see /docs for what's included.
How do I test an LLM integration without burning through my budget?
Use a small max_tokens value and short prompts during development, and test streaming and error paths with mocked responses before making live calls. Most billing surprises come from testing with production-length prompts, not from the integration code itself.