PHP cURL: save a Sume-generated image to disk with CURLOPT_FILE

Two cURL calls in plain PHP: POST to /v1/images, read data[0].url on a 200, then stream the file to disk with CURLOPT_FILE. Handles 202 and a missing key.

4 min readSume
All posts

Make two cURL handles. The first POSTs the JSON and reads data[0].url; the second downloads that URL straight into a file with CURLOPT_FILE. The curl_setopt manual lists the options you need: CURLOPT_RETURNTRANSFER to get the body back as a string, CURLOPT_POSTFIELDS for the JSON, CURLOPT_HTTPHEADER for the bearer header, CURLOPT_TIMEOUT for a ceiling, and CURLOPT_FILE for the output handle.

The Sume image API waits up to 30 seconds. A 200 carries data[].url; a 202 carries a job envelope instead and no image yet. Set CURLOPT_TIMEOUT above 30 so the wait itself cannot trip your own client.

What does the full script look like?

Run it as SUME_API_KEY=... php save.php. It exits early when the key is empty or when the status is not 200, and it prints the raw body so you can see the error envelope.

<?php
$key = getenv('SUME_API_KEY');
if (!$key) { fwrite(STDERR, "SUME_API_KEY is empty\n"); exit(1); }
$ch = curl_init('https://api.sume.com/v1/images');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_POSTFIELDS => json_encode([
    'model' => 'openai/gpt-image-2.5',
    'prompt' => 'a red kettle on a white table',
    'quality' => 'low',
    'output_format' => 'png',
  ]),
  CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key, 'Content-Type: application/json'],
  CURLOPT_TIMEOUT => 60,
]);
$raw = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code !== 200) { fwrite(STDERR, "status $code: $raw\n"); exit(1); }
$url = json_decode($raw, true)['data'][0]['url'];
$fp = fopen('out.png', 'wb');
$dl = curl_init($url);
curl_setopt_array($dl, [CURLOPT_FILE => $fp, CURLOPT_TIMEOUT => 60]);
curl_exec($dl);
curl_close($dl);
fclose($fp);
echo "saved out.png\n";

Why close the file after the handle?

The manual's note on CURLOPT_FILE is that the file must be closed after curl_close, which the script does in that order. Closing early can leave a truncated file. Check filesize('out.png') if you want a cheap sanity test before you use the image.

Setting CURLOPT_POSTFIELDS to a string makes cURL send a POST, so no separate flag is needed here. The Content-Type: application/json header is still required, because Sume expects a JSON body.

The 202 branch

Heavy settings (4K, high quality, large n) are the likeliest to miss the 30-second window. The script above treats that as a failure on purpose, to stay short. In production, when $code === 202, decode data.status_url and poll it with backoff until the status is terminal, then read result_url. The jobs guide is clear that you must not resubmit the create call because your own wait ended, since the first job keeps running and billing.

A 502 is a terminal failure of a sync request, and its body names a code and a next_action. Log both.

Keep the model fixed while you build the integration. The catalog is queryable at GET /v1/images/models, and a parameter that a model does not list is rejected with 400 unsupported_parameter, so a typo in output_format shows up on the first run instead of after a week of silent fallbacks. Store the job id from any 202 so a restarted PHP worker can pick the work up again instead of paying twice.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume