C# HttpClient and Sume images: keep the key off the download call
Reuse one HttpClient, but set the bearer header per request, not as a default header, or it also rides along when you download the Sume image URL.

Share one HttpClient, and put the Authorization header on the POST request, not on DefaultRequestHeaders. Microsoft's HttpClient reference says HttpClient is intended to be instantiated once per application, rather than per use. A shared client is where the trap lives: default headers apply to every request the client sends, so a bearer key set there would also go out on the second call that downloads the image from its hosted URL.
The Sume image API returns data[].url on a 200, and the file behind it needs no key. Send the key only to api.sume.com.
A runnable program
Use .NET 6 or later with top-level statements. It reads the key from the environment, builds the POST with a per-request header, branches on the status, and downloads with GetByteArrayAsync, which the HttpClient reference lists.
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
var key = Environment.GetEnvironmentVariable("SUME_API_KEY");
if (string.IsNullOrEmpty(key)) throw new Exception("SUME_API_KEY is empty");
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(60) };
var json = JsonSerializer.Serialize(new {
model = "openai/gpt-image-2.5", prompt = "a red kettle on a white table",
quality = "low", output_format = "png" });
var req = new HttpRequestMessage(HttpMethod.Post, "https://api.sume.com/v1/images") {
Content = new StringContent(json, Encoding.UTF8, "application/json") };
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", key);
var res = await http.SendAsync(req);
var text = await res.Content.ReadAsStringAsync();
if ((int)res.StatusCode != 200) { Console.WriteLine($"{(int)res.StatusCode}: {text}"); return; }
var url = JsonDocument.Parse(text).RootElement
.GetProperty("data")[0].GetProperty("url").GetString()!;
await File.WriteAllBytesAsync("out.png", await http.GetByteArrayAsync(url));
Console.WriteLine("saved out.png");What about the 202 case?
The program prints and returns on anything but 200, to stay short. A 202 means the job outlived the 30-second wait; its body has data.status_url and data.result_url. Poll the first with a growing delay until the status is completed, failed or canceled, then read the second. The jobs guide says never to resubmit the create request because a local timer expired.
Set the client timeout above 30 seconds, as the code does with 60, so your own deadline does not cancel a job the server is still holding open. The response's usage.cost is the billed amount in USD for the call.
If you register the client in dependency injection, use the factory pattern your framework provides and still set the bearer header on each message. That keeps one place that knows the key, and the generated image download stays a plain anonymous GET. Parse errors the same way: a 400 unsupported_parameter body tells you which parameter the model's catalog does not list, and retrying it unchanged will not help. Treat 5xx replies from the download host as retryable with a short backoff, since the image already exists by then and only the fetch failed.
Sources
Related posts
More in Developers
- C# HttpClient for Sume video jobs: a Sora port in 30 lines
A .NET top-level program that posts to Sume /v1/videos with an Idempotency-Key, polls to a terminal status, and saves the mp4. For C# teams that wrapped Sora.
- curl and jq: poll a Seedance 2.5 clip from a bash script
A bash script with curl and jq that submits a 30-second Seedance 2.5 job to Sume, polls every 30 seconds and saves the MP4, with costs at three resolutions.
- Smoke-test a video API key with curl: a $0.11 Omni clip
A 19-line curl and jq script: submit a 3-second 360p Gemini Omni Flash 1.1 job, poll it, save the MP4. At $0.0375 a second the test costs $0.1125.
- curl and jq video step for CI with exit codes 0, 2 and 3
A bash script that submits to Sume POST /v1/videos, polls with jq, and exits 2 on a rejected submit and 3 on a failed render so a CI job can tell them apart.
Written by Sume