LLM API Integration: A Practical Developer's Guide
What "LLM API Integration" Actually Means
LLM API integration is the process of wiring a large language model into your application's backend so it can send prompts, receive completions, and handle the surrounding plumbing — authentication, streaming, error handling, rate limits, and observability. It's not just "call an endpoint and print the response." A production-ready integration needs to survive network failures, handle long-running streams, track token usage, and stay maintainable as your product grows.
If you're evaluating how to do this for the first time, the short answer is: pick an API-compatible provider, authenticate with a bearer token, send structured messages (system prompt + conversation history), and design your backend to handle both streaming and non-streaming responses. The rest of this article walks through each piece with working examples.
The Core Request/Response Loop
Every LLM API integration, regardless of provider, follows the same basic shape:
- Build a request with a system prompt, conversation history, and generation parameters (max tokens, temperature).
- Send it as an authenticated HTTPS POST.
- Parse the response — either a single JSON payload or a stream of server-sent events.
- Handle errors, retries, and rate limits.
- Log usage (tokens in/out, latency, cost) for monitoring.
A minimal non-streaming call looks like this:
curl https://api.subtoapi.app/v1/messages \
-H "Authorization: Bearer $SUBTOAPI_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-sonnet-4",
"max_tokens": 1024,
"messages": [
{"role": "user", "content": "Summarize this ticket in one sentence."}
]
}'
That single request already touches four integration concerns: authentication (the bearer token), payload structure (messages array), model selection, and response parsing. Get the plumbing right once and every feature you build on top — chat, summarization, agents — reuses the same foundation.
Authentication and Key Management
Most LLM APIs use a bearer token in the Authorization header. The integration mistake teams make most often is hardcoding keys into client-side code or committing them to version control. Keep keys server-side, rotate them per environment (dev/staging/prod), and scope them per application where possible.
If you're building on top of a Claude subscription rather than a raw provider API, tools like SubToAPI turn that access into standard sub_live_... application keys you can issue per app or per environment, with usage tracked centrally in a dashboard instead of scattered across scripts. See /docs/quickstart for the exact setup steps.
Streaming: Don't Bolt It On Later
Non-streaming responses are simpler to integrate but produce a noticeably worse user experience for anything conversational — users wait for the entire completion before seeing a single word. Streaming sends the response incrementally as server-sent events, which lets you render tokens as they arrive.
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: "Draft a release note for v2.3." }],
}),
});
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));
}
The architectural decision to make early is whether streaming flows all the way to your frontend or terminates at your backend. Streaming end-to-end (backend to browser via SSE or WebSocket) gives the best UX but adds complexity in connection handling, reconnects, and proxy timeouts. Buffering server-side and returning a complete response is simpler but slower to first byte. Full details on event formats are in /docs/streaming.
Tool Use and Structured Output
Many real integrations aren't just "chat" — they need the model to call functions: look up a record, run a calculation, query an API. This is done by declaring tools (name, description, JSON schema for inputs) in the request and handling the model's tool-call response by executing the function and returning the result in a follow-up message.
{
"model": "claude-sonnet-4",
"max_tokens": 1024,
"tools": [
{
"name": "get_order_status",
"description": "Look up the status of a customer order by ID",
"input_schema": {
"type": "object",
"properties": {
"order_id": { "type": "string" }
},
"required": ["order_id"]
}
}
],
"messages": [
{ "role": "user", "content": "Where is order 48213?" }
]
}
The integration loop for tool use is: send request → model returns a tool call instead of text → your backend executes the tool → you send the result back as a new message → model produces a final answer. This round-trip is the backbone of most agentic features. See /docs/tools for the full schema and multi-turn examples.
Error Handling and Rate Limits
LLM APIs fail in predictable ways: rate limit errors (429), overloaded model errors (529/503), context length exceeded, and malformed requests. A resilient integration:
- Retries transient errors (429, 5xx) with exponential backoff.
- Fails fast on non-retryable errors (400, invalid schema).
- Sets sane timeouts — long generations can legitimately take 30+ seconds.
- Logs the request ID or trace ID returned in error responses for debugging.
Don't retry blindly. A 400 error means your request is malformed — retrying it wastes time and money without changing the outcome.
Monitoring Usage and Cost
Token usage is the main cost driver, and it's easy to lose visibility once multiple services or team members are calling the API independently. Track input/output tokens per request, tag them by feature or endpoint, and set alerts on unusual spikes. Full docs for reading usage metadata off every response are at /docs/messages.
If you're consolidating access across a team — multiple developers, multiple apps, one Claude subscription — a dashboard that shows per-key usage avoids the spreadsheet-tracking problem entirely. SubToAPI issues separate API keys per application with usage broken down per key, which is useful once more than one project shares the same underlying access. Plans start at €9/month for solo use, with team and scale tiers for shared seats — see /pricing.
A Practical Integration Checklist
- Keep API keys server-side, never in client bundles.
- Support both streaming and non-streaming code paths.
- Implement retry logic with backoff for 429/5xx errors.
- Validate and cap
max_tokensto control cost per request. - Log token usage per request for cost attribution.
- Add tool use only once basic chat is stable — it adds a second round trip and more failure modes.
- Test with realistic conversation lengths, not just single-turn prompts.
Start with the smallest working loop — one endpoint, one model, no streaming — then add streaming, tools, and monitoring incrementally. Trying to build all of it at once is where most integration projects stall.
Questions
Do I need a different integration for streaming vs non-streaming? No — same endpoint, same request body, just set stream: true and parse server-sent events instead of a single JSON payload. Design your backend to handle both from the start so you're not refactoring later.
How do I test an LLM API integration without burning through tokens? Use short max_tokens limits, cheap/fast models for development, and mock responses for unit tests. Reserve real API calls for integration tests and manual QA.
Can I integrate an LLM API without managing raw provider keys myself? Yes — services like SubToAPI sit between your app and your Claude access, giving you standard bearer-token API keys per application with usage tracking, so you integrate against one consistent API instead of managing raw credentials across projects. See /docs to get started.