n4nAI

Calling the OpenAI API from C# with HttpClient

Learn how to call the OpenAI API from C# using HttpClient with a hands-on tutorial covering auth, requests, streaming, and error handling.

n4n Team2 min read451 words

Audio narration

Coming soon — every post will get a voice note here.

Most .NET teams reach for a heavyweight SDK when they need LLM access, but a bare c# httpclient openai api call is often all you need for production control. This tutorial builds a minimal, resilient client from scratch—no NuGet packages beyond the framework itself.

Prerequisites

  • .NET 6 SDK or newer (tested on .NET 8).
  • An OpenAI API key (or a key for any OpenAI-compatible endpoint).
  • Comfort with async/await and basic JSON.

You do not need the OpenAI NuGet package. We use System.Net.Http and System.Text.Json, both in the shared framework.

Project Setup

Create a console app and trim the boilerplate:

dotnet new console -o OpenAiRaw
cd OpenAiRaw

Replace Program.cs with a skeleton that reads the key from an environment variable:

using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;

var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY")
    ?? throw new InvalidOperationException("Set OPENAI_API_KEY");

var client = new HttpClient
{
    BaseAddress = new Uri("https://api.openai.com/v1/")
};
client.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", apiKey);

Console.WriteLine("Client ready");

Run dotnet run. Expected output:

Client ready

Building the Request Payload

OpenAI’s chat endpoint expects a JSON body with model and messages. Define records to avoid stringly-typed code:

record ChatMessage(string Role, string Content);
record ChatRequest(string Model, ChatMessage[] Messages);

A minimal call sends a single user message:

var request = new ChatRequest(
    Model: "gpt-3.5-turbo",
    Messages: new[] { new ChatMessage("user", "Say hello in 5 words.") });

var json = JsonSerializer.Serialize(request);
var content = new StringContent(json, Encoding.UTF8, "application/json");

Sending Your First Chat Request

Post to chat/completions and inspect the raw response:

var response = await client.PostAsync("chat/completions", content);
response.EnsureSuccessStatusCode();

var responseJson = await response.Content.ReadAsStringAsync();
Console.WriteLine(responseJson);

Expected output (trimmed):

{
  "id":"chatcmpl-...",
  "object":"chat.completion",
  "choices":[
    {"index":0,"message":{"role":"assistant","content":"Hello! Hope you're well today."},"finish_reason":"stop"}
  ],
  "usage":{"prompt_tokens":12,"completion_tokens":6,"total_tokens":18}
}

Parse it into typed shapes so the rest of your app stays clean:

record Choice(ChatMessage Message, string FinishReason);
record ChatResponse(string Id, Choice[] Choices);

var parsed = JsonSerializer.Deserialize<ChatResponse>(responseJson);
Console.WriteLine(parsed!.Choices[0].Message.Content);

Error Handling and Status Codes

A c# httpclient openai api integration fails in predictable ways: 401 for bad keys, 429 for rate limits, 5xx for upstream issues. Do not swallow EnsureSuccessStatusCode blindly. Read the error body:

if (!response.IsSuccessStatusCode)
{
    var errBody = await response.Content.ReadAsStringAsync();
    // OpenAI returns { "error": { "message": "...", "type": "..." } }
    throw new ApplicationException($"API {response.StatusCode}: {errBody}");
}

For transient 429/5xx, wrap the call in a simple retry loop or use Polly if you already reference it. Set a explicit timeout:

client.Timeout = TimeSpan.FromSeconds(30);

Streaming Tokens with Server-Sent Events

Non-streaming calls block until the full completion finishes. For chat UIs, stream instead. Add Stream = true to the request:

record StreamRequest(string Model, ChatMessage[] Messages, bool Stream = true);
var streamReq = new StreamRequest("gpt-3.5-turbo",
    new[] { new ChatMessage("user", "Count to 5 slowly.") });

var streamContent = new StringContent(
    JsonSerializer.Serialize(streamReq), Encoding.UTF8, "application/json");

var streamResponse = await client.PostAsync("chat/completions", streamContent);
streamResponse.EnsureSuccessStatusCode();

Read the response stream line by line. OpenAI emits data: {json} chunks and a final data: [DONE].

using var stream = await streamResponse.Content.ReadAsStreamAsync();
using var reader = new StreamReader(stream);

while (!reader.EndOfStream)
{
    var line = await reader.ReadLineAsync();
    if (string.IsNullOrWhiteSpace(line)) continue;
    if (!line.StartsWith("data:")) continue;

    var payload = line["data:".Length..].Trim();
    if (payload == "[DONE]") break;

    var chunk = JsonSerializer.Deserialize<JsonElement>(payload);
    var delta = chunk.GetProperty("choices")[0]
        .GetProperty("delta").GetProperty("content").GetString();
    if (delta is not null) Console.Write(delta);
}
Console.WriteLine();

Expected output prints tokens incrementally, e.g. 1... 2... 3... as they arrive.

Swapping Endpoints Without Rewriting Code

The request and response contracts above are stable across any OpenAI-compatible server. If you point BaseAddress at a gateway, the same c# httpclient openai api code works unchanged. For instance, n4n.ai exposes one OpenAI-compatible endpoint covering 240+ models and applies automatic fallback when a provider is degraded, so you can repoint BaseAddress to https://api.n4n.ai/v1/ and keep the serialization logic intact.

Production Considerations

  • HttpClient lifetime: In ASP.NET, inject IHttpClientFactory instead of a static instance to avoid socket exhaustion.
  • Cancellation: Pass a CancellationToken from PostAsync to respect request timeouts and user navigation.
  • Model pinning: Store model in config, not code. The same payload works for gpt-4o, claude-3-* via a gateway, etc.
  • Logging: Log response.Headers.GetValues("x-request-id") (OpenAI sends it) for support correlation.

A hardened factory registration looks like:

builder.Services.AddHttpClient("openai", c =>
{
    c.BaseAddress = new Uri("https://api.openai.com/v1/");
    c.DefaultRequestHeaders.Authorization =
        new AuthenticationHeaderValue("Bearer", builder.Configuration["OpenAiKey"]);
    c.Timeout = TimeSpan.FromSeconds(30);
});

Then resolve IHttpClientFactory and create a client per logical call.

Wrap-Up

You now have a working c# httpclient openai api client that sends chat requests, parses typed responses, streams tokens, and handles errors—without external SDKs. That control matters when you need custom retries, metrics, or multi-provider routing. Keep the payload contracts in records, treat the network as hostile, and you can ship this into a service today.

Tagscsharpdotnethttpclientopenai-api

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All c# / .net llm api integration posts →