How to Call an LLM API: The Complete Mechanics
Calling an LLM API means sending an HTTP request with your prompt and configuration to a provider's endpoint, then handling the response — either as a single JSON payload or as a stream of tokens. The mechanics are the same across providers: authenticate with an API key, POST a JSON body describing the conversation, and parse whatever comes back.
This article walks through that request/response cycle in detail: what goes in the headers, what the body needs to contain, how streaming changes the flow, and the errors you should expect to handle. The examples use curl and JavaScript, but the pattern applies whether you're calling OpenAI, Anthropic, or a proxy like SubToAPI.
The Anatomy of an LLM API Call
Every LLM API call has four parts:
- Endpoint URL — where the request goes (usually a
/messagesor/chat/completionspath) - Headers — authentication and content type
- Request body — model name, messages, and parameters like
max_tokensortemperature - Response handling — parsing JSON or reading a stream
Here's a minimal curl call against SubToAPI's Messages endpoint, which follows Anthropic's message format:
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": "Explain what a race condition is."}
]
}'
The response comes back as JSON with the generated text, a stop reason, and token usage:
{
"id": "msg_01abc...",
"content": [{"type": "text", "text": "A race condition occurs when..."}],
"stop_reason": "end_turn",
"usage": {"input_tokens": 12, "output_tokens": 84}
}
Full request/response field details are in the Messages docs.
Calling the API from Code
In practice you'll call the API from your backend, not from a terminal. Here's a Node.js example using fetch:
async function askClaude(prompt) {
const res = 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: 512,
messages: [{ role: "user", content: prompt }]
})
});
if (!res.ok) {
throw new Error(`API error ${res.status}: ${await res.text()}`);
}
const data = await res.json();
return data.content[0].text;
}
Three things matter here that beginners often skip:
- Always check
res.okbefore parsing the body — a 401 or 429 won't throw on its own withfetch. - Set
max_tokensdeliberately. It caps cost and latency; leaving it unset (where allowed) or set too high wastes both. - Never hardcode the key. Load it from an environment variable, and never commit it to source control.
If you're just getting started, walk through the quickstart guide — it covers key generation and your first successful call end to end.
Streaming Responses
For anything user-facing — chat UIs, CLI tools — you don't want to wait for the full response before showing anything. Streaming sends the response as a series of chunks (usually Server-Sent Events) as tokens are generated.
const res = 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: 512,
stream: true,
messages: [{ role: "user", content: "Write a haiku about databases." }]
})
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
process.stdout.write(decoder.decode(value));
}
Streaming does not change the endpoint or the authentication — only the stream flag and how you read the response body. Details on event types and parsing are in the streaming docs.
Calling Tools from the LLM
Most modern LLM APIs support tool use (also called function calling): you describe available functions in the request, and the model responds with a structured request to call one, which your code executes and feeds back.
{
"model": "claude-sonnet-4",
"max_tokens": 512,
"tools": [
{
"name": "get_weather",
"description": "Get current weather for a city",
"input_schema": {
"type": "object",
"properties": { "city": { "type": "string" } },
"required": ["city"]
}
}
],
"messages": [{ "role": "user", "content": "What's the weather in Lisbon?" }]
}
The model's response will include a tool_use block instead of (or alongside) text. Your code runs the actual function, then sends the result back as a follow-up message so the model can incorporate it into a final answer. This is a multi-turn call pattern, not a single request — plan for the extra round trip in your latency budget. See the tools docs for the full schema and flow.
Handling Errors and Rate Limits
Production code calling an LLM API needs to handle, at minimum:
- 429 (rate limited) — back off and retry with exponential delay
- 401/403 (auth) — check the key is valid and correctly scoped
- 400 (bad request) — usually a malformed message array or missing required field
- 5xx — provider-side issue; retry a few times before failing
async function callWithRetry(fn, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
return await fn();
} catch (err) {
if (i === retries - 1) throw err;
await new Promise(r => setTimeout(r, 2 ** i * 500));
}
}
}
If you're calling the API on behalf of multiple users or products, using distinct application keys (like SubToAPI's sub_live_... keys) instead of one shared key makes it much easier to trace which caller triggered which error, and to check per-key usage without digging through logs. You can generate and manage keys from the dashboard after signing up.
Wrapping Up
Calling an LLM API is a standard HTTP POST with a JSON body — the complexity is in message formatting, streaming, tool-call round trips, and retry logic, not the transport itself. Get the basic request working first, then layer in streaming and tools once the core call is solid. Check pricing if you're comparing hosted options for turning existing Claude access into an API you can call from your own code.
Questions
Do I need a special SDK to call an LLM API? No. Any HTTP client works — curl, fetch, axios, Python's requests. SDKs just wrap the same JSON request/response pattern with typed helpers.
Why did my API call return a 429 error? You've hit a rate or usage limit. Implement exponential backoff and retry, and check your plan's limits if it happens consistently.
What's the difference between calling with streaming on vs off? Non-streaming waits for the full response then returns one JSON object. Streaming sends the response in chunks as it's generated, letting you display output incrementally.