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.

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 AcceptedWhich 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.
| Option | What the PHP manual says | Set it to |
|---|---|---|
CURLOPT_POST | Does an HTTP POST with the form-urlencoded header; defaults to false | true, plus your own Content-Type |
CURLOPT_POSTFIELDS | An array sets Content-Type to multipart/form-data | The json_encode string |
CURLOPT_RETURNTRANSFER | Makes curl_exec() return the body as a string | true |
CURLOPT_HTTPHEADER | An array of header lines | Content-Type, Authorization, Idempotency-Key |
CURLOPT_FAILONERROR | Off by default: a status of 400 or more comes back as a normal page | Leave off and read the status yourself |
CURLOPT_TIMEOUT | Defaults to 0: never times out during transfer | A number of seconds |
CURLOPT_CONNECTTIMEOUT | Defaults to 300 seconds | A 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 bothAuthorization: Bearerandx-api-key. Send exactly one.402 insufficient_credits: the balance can't cover the generation.429 rate_limitedor429 queue_full: back off, and useretry-afterwhen present. On PHP 8.2+ with cURL 7.66.0+,CURLINFO_RETRY_AFTERreturns 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
- Polly retry policy for an HttpClient POST to a paid API
A Polly retry for a paid POST: handle only transient failures, back off exponentially with jitter, honor Retry-After, and resend one idempotency key.
- Power Automate HTTP Webhook action: wait for a callback
The HTTP Webhook action sends a subscribe request with the flow's callback URL, then pauses until something POSTs to it. How to use it with a slow API.
- Spring Boot RestTemplate POST JSON with a Bearer token
Set JSON content type and setBearerAuth on HttpHeaders, wrap them in an HttpEntity, call postForEntity, and catch the 4xx exception to read the body.
- Salesforce Apex HTTP callout: POST JSON to an external API
An Apex HTTP callout builds an HttpRequest, sends it with Http.send and reads the HttpResponse. From a trigger or after DML, it must run asynchronously.
Written by Sume