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.

5 min readSume
All posts

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

All Developers posts

Written by Sume