How to Call the OpenAI API from C#: A Complete Guide for .NET Developers
Almost every LLM tutorial on the internet starts with pip install. If you're a .NET developer, it's easy to conclude that AI development belongs to Python and your stack is a second-class citizen. It isn't. OpenAI ships an official .NET SDK, the APIs are plain HTTPS underneath, and C# is a genuinely great language for building production AI features — you already have the typing, the async story, and the DI ecosystem.
This is the guide I wish I'd had when I added my first LLM feature to an ASP.NET Core service: from zero to a streaming, error-handled chat completion, all in C#.
Setup
You'll need an API key from the OpenAI platform (Dashboard → API keys). Then install the official package:
dotnet add package OpenAI
Never hardcode the key. Locally, use user secrets; in production, environment variables or a secret store (Azure Key Vault):
dotnet user-secrets init
dotnet user-secrets set "OpenAI:ApiKey" "sk-..."
Your first chat completion
using OpenAI.Chat;
var client = new ChatClient(
model: "gpt-4o-mini",
apiKey: configuration["OpenAI:ApiKey"]);
ChatCompletion completion = await client.CompleteChatAsync(
new SystemChatMessage(
"You are a concise assistant for a South African e-commerce store. " +
"Answer in two sentences or fewer."),
new UserChatMessage("What's your returns policy?"));
Console.WriteLine(completion.Content[0].Text);
Three things worth understanding immediately:
- The system message is your steering wheel. Tone, format, constraints, persona — set them here, not by hoping the user asks nicely.
- Models are named strings.
gpt-4o-miniis cheap and fast — right for most features; step up to a larger model only where quality measurably demands it. - The API is stateless. The model remembers nothing between calls. "Conversation memory" is you sending the history back every time:
var messages = new List<ChatMessage>
{
new SystemChatMessage("You are a helpful support agent."),
};
// each turn:
messages.Add(new UserChatMessage(userInput));
ChatCompletion reply = await client.CompleteChatAsync(messages);
messages.Add(new AssistantChatMessage(reply.Content[0].Text));
That growing list is also your growing bill — long conversations eventually need summarizing or trimming, because you pay per token, every turn.
Wiring it into ASP.NET Core properly
ChatClient is thread-safe; register it once as a singleton behind your own interface so the rest of the codebase isn't coupled to a vendor SDK:
public interface IAssistantService
{
Task<string> AskAsync(string question, CancellationToken ct = default);
}
public class OpenAiAssistantService : IAssistantService
{
private readonly ChatClient _client;
public OpenAiAssistantService(IConfiguration config)
=> _client = new ChatClient("gpt-4o-mini", config["OpenAI:ApiKey"]);
public async Task<string> AskAsync(string question, CancellationToken ct = default)
{
ChatCompletion completion = await _client.CompleteChatAsync(
[new SystemChatMessage(SystemPrompt), new UserChatMessage(question)],
cancellationToken: ct);
return completion.Content[0].Text;
}
}
// Program.cs
builder.Services.AddSingleton<IAssistantService, OpenAiAssistantService>();
That interface seam matters more in AI code than most: providers leapfrog each other every few months, and you want swapping (or A/B testing) to be a one-class change. (If you want the abstraction handled for you, that's exactly what Microsoft.Extensions.AI provides — see the follow-up post linked below.)
Streaming: the difference between "broken" and "fast"
A full completion can take many seconds, and users perceive a silent spinner as failure. Streaming sends tokens as they're generated:
await foreach (StreamingChatCompletionUpdate update
in _client.CompleteChatStreamingAsync(messages, cancellationToken: ct))
{
foreach (var part in update.ContentUpdate)
{
await response.WriteAsync(part.Text, ct); // e.g. SSE to the browser
}
}
In ASP.NET Core, pair this with server-sent events or SignalR to get the ChatGPT-style typing effect. Same cost, same latency-to-last-token — dramatically better perceived speed. For any user-facing chat feature, streaming isn't optional polish; it's table stakes.
Errors and resilience: the part demos skip
LLM APIs fail in ways your other dependencies don't — rate limits are routine, latency is variable, and outages happen. Production checklist:
try
{
ChatCompletion completion = await _client.CompleteChatAsync(messages,
cancellationToken: ct);
return completion.Content[0].Text;
}
catch (ClientResultException ex) when (ex.Status == 429)
{
// Rate limited — back off and retry (or use Polly for the policy)
_logger.LogWarning("OpenAI rate limit hit");
throw new AssistantUnavailableException();
}
catch (ClientResultException ex)
{
_logger.LogError(ex, "OpenAI call failed with status {Status}", ex.Status);
throw new AssistantUnavailableException();
}
- Retry 429/5xx with exponential backoff (Polly's
AddResilienceHandlerif you're calling via HttpClient, or a simple retry loop). - Always pass the CancellationToken — users abandon slow requests; don't keep paying for them.
- Degrade gracefully. Your app should have an answer for "the AI is down" that isn't a 500.
- Log token usage (
completion.Usage) — it's your cost meter, and the first thing you'll want when the invoice surprises someone.
What it costs (and how not to get surprised)
You're billed per input and output token (a token ≈ ¾ of an English word). Small models cost fractions of a cent per typical request; the mistakes that produce scary invoices are always architectural: resending huge conversation histories, stuffing entire documents into every prompt, or retry loops without caps. Set a monthly budget limit in the OpenAI dashboard on day one, and log usage per feature so costs have an owner.
Where to go next
You now have the core loop every AI feature is built on: messages in, completion out, streamed to the user, with failure handling around it. From here:
- Getting Started with Microsoft.Extensions.AI — the provider-agnostic abstraction layer for .NET.
- Getting Reliable JSON Out of LLMs in C# — when you need data back, not prose.
- LLM Function Calling in C# — letting the model safely invoke your code.