Call the Claude API from C# using HttpClient. Complete working example with async/await, system prompt, streaming SSE, and exponential-backoff retry — no official .NET SDK required.
There is no official Anthropic .NET SDK. This guide shows the complete, production-ready pattern for calling the Claude API from C# using HttpClient and System.Text.Json — no third-party packages required.
// .NET 8 (recommended) or .NET 6+
// No NuGet packages beyond the BCL
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
var apiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY")
?? throw new InvalidOperationException("ANTHROPIC_API_KEY not set");
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("x-api-key", apiKey);
http.DefaultRequestHeaders.Add("anthropic-version", "2023-06-01");
var payload = new
{
model = "claude-sonnet-4-6",
max_tokens = 1024,
messages = new[] { new { role = "user", content = "Explain prompt caching in one paragraph." } }
};
var json = JsonSerializer.Serialize(payload);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await http.PostAsync("https://api.anthropic.com/v1/messages", content);
response.EnsureSuccessStatusCode();
var body = await response.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(body);
var text = doc.RootElement
.GetProperty("content")[0]
.GetProperty("text")
.GetString();
Console.WriteLine(text);
var payload = new
{
model = "claude-sonnet-4-6",
max_tokens = 512,
system = "You are a concise technical writer. Answer in bullet points.",
messages = new[]
{
new { role = "user", content = "What are the Claude API rate limits?" }
}
};
public class ClaudeClient : IDisposable
{
private readonly HttpClient _http;
private const string BaseUrl = "https://api.anthropic.com/v1/messages";
public ClaudeClient(string apiKey)
{
_http = new HttpClient();
_http.DefaultRequestHeaders.Add("x-api-key", apiKey);
_http.DefaultRequestHeaders.Add("anthropic-version", "2023-06-01");
}
public async Task<string> CompleteAsync(
string userMessage,
string? systemPrompt = null,
string model = "claude-sonnet-4-6",
int maxTokens = 1024)
{
var messages = new[] { new { role = "user", content = userMessage } };
object payload = systemPrompt is null
? new { model, max_tokens = maxTokens, messages }
: new { model, max_tokens = maxTokens, system = systemPrompt, messages };
var content = new StringContent(
JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
var resp = await _http.PostAsync(BaseUrl, content);
resp.EnsureSuccessStatusCode();
using var doc = JsonDocument.Parse(await resp.Content.ReadAsStringAsync());
return doc.RootElement.GetProperty("content")[0].GetProperty("text").GetString()!;
}
public void Dispose() => _http.Dispose();
}
// Usage:
using var claude = new ClaudeClient(
Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY")!);
string answer = await claude.CompleteAsync("What is a context window?");
Console.WriteLine(answer);
public async IAsyncEnumerable<string> StreamAsync(string userMessage)
{
var payload = new
{
model = "claude-sonnet-4-6",
max_tokens = 1024,
stream = true,
messages = new[] { new { role = "user", content = userMessage } }
};
var req = new HttpRequestMessage(HttpMethod.Post, BaseUrl)
{
Content = new StringContent(
JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json")
};
using var resp = await _http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
resp.EnsureSuccessStatusCode();
using var stream = await resp.Content.ReadAsStreamAsync();
using var reader = new StreamReader(stream);
while (!reader.EndOfStream)
{
var line = await reader.ReadLineAsync();
if (string.IsNullOrEmpty(line) || !line.StartsWith("data:")) continue;
var data = line.Substring(5).Trim();
if (data == "[DONE]") break;
using var doc = JsonDocument.Parse(data);
var root = doc.RootElement;
if (root.TryGetProperty("delta", out var delta) &&
delta.TryGetProperty("text", out var textEl))
{
yield return textEl.GetString() ?? "";
}
}
}
// Usage:
var sb = new StringBuilder();
await foreach (var chunk in claude.StreamAsync("Write a haiku about APIs."))
{
Console.Write(chunk);
sb.Append(chunk);
}
Console.WriteLine();
string fullResponse = sb.ToString();
public async Task<string> CompleteWithRetryAsync(string message, int maxRetries = 4)
{
for (int attempt = 0; attempt <= maxRetries; attempt++)
{
try { return await CompleteAsync(message); }
catch (HttpRequestException ex) when (attempt < maxRetries)
{
// Retry on 429 (rate limit) and 529 (overloaded)
if (ex.StatusCode is not (System.Net.HttpStatusCode.TooManyRequests
or (System.Net.HttpStatusCode)529))
throw;
var delay = TimeSpan.FromSeconds(Math.Pow(2, attempt))
+ TimeSpan.FromMilliseconds(Random.Shared.Next(0, 200));
await Task.Delay(delay);
}
}
throw new InvalidOperationException("Max retries exceeded");
}
| Model ID | Input $/MTok | Output $/MTok | Best for |
|---|---|---|---|
claude-haiku-4-5-20251001 | $0.80 | $4.00 | High-volume, latency-sensitive |
claude-sonnet-4-6 | $3.00 | $15.00 | Most workloads (best price/quality) |
claude-opus-4-7 | $15.00 | $75.00 | Long-context reasoning, highest quality |
Estimate costs for your .NET app at the Claude API Cost Calculator. For more API patterns, see the error handling and retry guide and rate limits explained.