PHP cURL POST JSON with a Bearer token

json_encode the body, pass the string to CURLOPT_POSTFIELDS, set Content-Type and Authorization headers, then check the status: cURL won't fail on a 4xx.

5 min readSume
All posts

To POST JSON with PHP cURL, json_encode the body and pass that string to CURLOPT_POSTFIELDS, set CURLOPT_POST and CURLOPT_RETURNTRANSFER to true, and put Content-Type: application/json and Authorization: Bearer <token> in CURLOPT_HTTPHEADER. Then read the status with curl_getinfo($ch, CURLINFO_RESPONSE_CODE), because by default cURL returns a 4xx or 5xx page normally instead of failing.

cURL facts come from the PHP manual pages for curl_setopt, cURL constants and curl_exec. The example API is Sume's, from Authentication, Video Generation, Errors and rate limits and Jobs and results, all read on 2026-09-29. Sume's only documented client library is the TypeScript SDK, so from PHP this is a plain HTTPS call.

How do I send a JSON POST with a Bearer token in PHP cURL?

This starts a Sume video job. The key comes from the server environment: Sume's docs say not to place API keys in frontend JavaScript, mobile apps, support tickets or screenshots, so don't print it in a WordPress settings page either.

<?php
$body = json_encode([
    'model' => 'sume/auto',
    'prompt' => 'A vertical product clip on a desk, natural light',
    'aspect_ratio' => '9:16',
    'duration' => 5,
], JSON_THROW_ON_ERROR);

$ch = curl_init('https://api.sume.com/v1/videos');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => $body,          // a string, not an array
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CONNECTTIMEOUT => 10,
    CURLOPT_TIMEOUT => 30,
    CURLOPT_HTTPHEADER => [
        'Content-Type: application/json',
        'Authorization: Bearer ' . getenv('SUME_API_KEY'),
        'Idempotency-Key: order-8823-clip-v1',
    ],
]);
$raw = curl_exec($ch);
if ($raw === false) throw new RuntimeException(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode($raw, true);
if ($status >= 400) throw new RuntimeException("$status {$data['error']['code']}");
echo $data['id'], ' ', $data['polling_url'];   // 202 Accepted

Which cURL options matter for a JSON POST?

The common mistake is passing a PHP array to CURLOPT_POSTFIELDS. That sends multipart/form-data, and a JSON API such as Sume's answers 415 unsupported_media_type when the body isn't application/json.

From the PHP manual's cURL constants and curl_exec pages, read 2026-09-29.
OptionWhat the PHP manual saysSet it to
CURLOPT_POSTDoes an HTTP POST with the form-urlencoded header; defaults to falsetrue, plus your own Content-Type
CURLOPT_POSTFIELDSAn array sets Content-Type to multipart/form-dataThe json_encode string
CURLOPT_RETURNTRANSFERMakes curl_exec() return the body as a stringtrue
CURLOPT_HTTPHEADERAn array of header linesContent-Type, Authorization, Idempotency-Key
CURLOPT_FAILONERROROff by default: a status of 400 or more comes back as a normal pageLeave off and read the status yourself
CURLOPT_TIMEOUTDefaults to 0: never times out during transferA number of seconds
CURLOPT_CONNECTTIMEOUTDefaults to 300 secondsA few seconds

How do I read the error from the JSON response?

curl_exec() returns false on failure, and the manual says to compare with ===; read curl_error() for the reason. With CURLOPT_FAILONERROR off, an HTTP error status comes back as a normal page, so check CURLINFO_RESPONSE_CODE, then json_decode the body. Sume's errors share one envelope, {"error": {"code", "message", "request_id", "details"}}, and the request id is safe to share with Sume support.

  • 401 unauthorized: the key is missing or invalid, or the request sent both Authorization: Bearer and x-api-key. Send exactly one.
  • 402 insufficient_credits: the balance can't cover the generation.
  • 429 rate_limited or 429 queue_full: back off, and use retry-after when present. On PHP 8.2+ with cURL 7.66.0+, CURLINFO_RETRY_AFTER returns that header's value, or zero.

Why does the POST return before the video exists?

Video generation is asynchronous. POST /v1/videos answers 202 Accepted with an id, a polling_url and status: "pending"; you then poll GET /v1/videos/{id} until the status is completed. Don't raise CURLOPT_TIMEOUT to the length of the render: a client-side timeout doesn't cancel the job, which keeps running and still bills.

That is also why the Idempotency-Key header is there. On /v1/videos, a replay with the same key returns the original job, so a retried request after a timeout doesn't start a second paid one. Sume's docs say not to retry unsafe submit requests without one. Guzzle retry middleware for POST covers retries, and PHP webhook signature verification covers the callback side.

Sources

Related posts

More in Integrations

All Integrations posts

Written by Sume