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.

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
- PHP cURL: submit a Seedance 2.5 video job, poll it, save the MP4
PHP with only ext-curl: POST /v1/videos for seedance-2.5, poll polling_url until completed, then download content?index=0; a 5-second 480p test costs $1.34.
- Pick a Sume video model by script: filter /v1/videos/models
Instead of guessing, call GET /v1/videos/models and filter by ratio, resolution and last frame. A 15-line Python script lists the Sume video models that fit.
- Pick an image model by capability from the Sume catalog in Python
Filter GET /v1/images/models by reference count, ratio and resolution tier instead of hard-coding ids. A short Python function that runs as written.
- Pin the Sume CLI to a release tag in CI and check for updates
Install a fixed sume binary from GitHub Releases, verify checksums.txt, and run sume update --check to see a newer release without changing files.
Written by Sume