API and development
Process images with the SocialCutter API from PHP
SocialCutter API client in PHP with cURL and Guzzle: health, platforms, processing by URL and file with CURLFile, download, history and wallet.
- PHP
- cURL
- Guzzle
- API
- SocialCutter
- CURLFile
- API key
Requirements
PHP with the cURL extension (shipped by default on most installs). If you prefer Guzzle, install it with Composer:
composer require guzzlehttp/guzzle
export SOCIALCUTTER_API_URL="https://api.socialcutter.theboomer.dev"
export SOCIALCUTTER_API_KEY="sc_your_key"
Create the key at https://dash.socialcutter.theboomer.dev under Profile → API keys. It starts with sc_ and is shown only once. The full reference is at https://docs.socialcutter.theboomer.dev.
Minimal client with cURL
<?php
$apiUrl = getenv('SOCIALCUTTER_API_URL') ?: 'https://api.socialcutter.theboomer.dev';
$apiKey = getenv('SOCIALCUTTER_API_KEY'); // never hardcode it
function apiCall(string $method, string $path, ?array $json = null, array $multipart = []): array
{
global $apiUrl, $apiKey;
$ch = curl_init($apiUrl . $path);
$headers = ['X-API-Key: *** . $apiKey, 'Accept: application/json'];
if ($json !== null) {
$headers[] = 'Content-Type: application/json';
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($json));
} elseif ($multipart) {
curl_setopt($ch, CURLOPT_POSTFIELDS, $multipart); // cURL writes the boundary
}
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 60,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($body === false) {
throw new RuntimeException('cURL: ' . curl_error($ch));
}
curl_close($ch);
$data = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("HTTP $status: $body");
}
return $data;
}
The key travels in the X-API-Key header. Authorization: Bearer sc_... is accepted too.
Health and credentials
// Public: no key required
print_r(apiCall('GET', '/api/v1/health')); // status, version, uptime, database
// Identity of the authenticated account
print_r(apiCall('GET', '/api/v1/auth/me'));
// Available uses
print_r(apiCall('GET', '/api/v1/credits'));
If /api/v1/auth/me returns 401, the key is missing, malformed or revoked.
List platforms and formats
$data = apiCall('GET', '/api/v1/platforms');
foreach ($data['platforms'] as $platform => $formats) {
foreach ($formats as $f) {
echo "$platform {$f['format']} {$f['width']}x{$f['height']} {$f['aspect_ratio']}\n";
}
}
/platforms, /formats and /fit-modes are public. Use them to build destinations without hardcoding dimensions.
Process an image by URL
$resp = apiCall('POST', '/api/v1/images/process', [
'source' => ['type' => 'url', 'value' => 'https://example.com/photo.jpg'],
'destinations' => [
['platform' => 'instagram', 'format' => 'post'],
['platform' => 'tiktok', 'format' => 'cover'],
],
]);
echo $resp['image_id'], ' ', $resp['status'], "\n";
Process a local file with CURLFile
$resp = apiCall('POST', '/api/v1/images/process/upload', null, [
'file' => new CURLFile('./photo.jpg', 'image/jpeg', 'photo.jpg'),
'destinations' => json_encode([['platform' => 'linkedin', 'format' => 'post']]),
]);
With CURLFile do not set Content-Type yourself: cURL generates the multipart boundary. The file goes in the file field and destinations is a JSON string. The upload limit is 5 MB; above that the API returns 413.
Process a batch
$batch = apiCall('POST', '/api/v1/images/batch', [
'images' => [
['source' => ['type' => 'url', 'value' => 'https://example.com/a.jpg'],
'destinations' => [['platform' => 'instagram', 'format' => 'post']]],
['source' => ['type' => 'url', 'value' => 'https://example.com/b.jpg'],
'destinations' => [['platform' => 'twitter', 'format' => 'post']]],
],
]);
The same with Guzzle
Guzzle simplifies error handling and multipart:
<?php
require 'vendor/autoload.php';
use GuzzleHttp\Client;
$client = new Client([
'base_uri' => getenv('SOCIALCUTTER_API_URL') . '/',
'headers' => ['X-API-Key' => getenv('SOCIALCUTTER_API_KEY')],
'timeout' => 60,
]);
// By URL
$resp = $client->post('api/v1/images/process', [
'json' => [
'source' => ['type' => 'url', 'value' => 'https://example.com/photo.jpg'],
'destinations' => [['platform' => 'instagram', 'format' => 'post']],
],
])->json();
// Local file (multipart)
$resp = $client->post('api/v1/images/process/upload', [
'multipart' => [
['name' => 'file', 'contents' => fopen('./photo.jpg', 'r'), 'filename' => 'photo.jpg'],
['name' => 'destinations', 'contents' => json_encode([['platform' => 'linkedin', 'format' => 'post']])],
],
])->json();
In Guzzle, json sends a JSON body and multipart builds the form. Documented pattern at https://docs.guzzlephp.org/en/stable/request-options.html; options and names can change between versions, so confirm them in that documentation.
Read the response and download
The response carries the job id in image_id, a status and outputs, one entry per destination with platform, format, url, width, height and size_bytes.
echo $resp['image_id'], "\n";
foreach ($resp['outputs'] as $out) {
echo "{$out['platform']} {$out['format']} {$out['width']}x{$out['height']}\n";
$bytes = file_get_contents($out['url']);
file_put_contents("{$out['platform']}-{$out['format']}.webp", $bytes);
}
History and wallet
// Last 10 images
print_r(apiCall('GET', '/api/v1/history?limit=10'));
// Only those created from the API
print_r(apiCall('GET', '/api/v1/history?origin=api&limit=10'));
// Daily quota, extra pool and purchased balance
print_r(apiCall('GET', '/api/v1/wallet'));
limit accepts 1 to 100 and skip pages through results. The origin filter separates browser (dashboard) from api.
Errors and retries with backoff
function withRetries(callable $fn, int $attempts = 4, int $baseMs = 500)
{
for ($i = 0; $i < $attempts; $i++) {
try {
return $fn();
} catch (RuntimeException $e) {
preg_match('/HTTP (\d+)/', $e->getMessage(), $m);
$code = (int) ($m[1] ?? 0);
if (($code === 429 || $code >= 500) && $i < $attempts - 1) {
usleep($baseMs * 1000 * (2 ** $i)); // 0.5, 1, 2, 4 s
continue;
}
throw $e;
}
}
}
Do not retry 400, 401, 404 or 422: repeating the same request will not fix them. Only 429 (quota) and 5xx (temporary failure) deserve another attempt.
Cost
- 1 use per destination (platform and format) per request.
- Repeated destinations in the same request are not charged twice.
- Failed processings are refunded.
Typical errors
| Code | Meaning |
|---|---|
| 400 | Invalid payload: unknown platform, format or mode, malformed JSON, or a key already active |
| 401 | Missing, malformed, expired or revoked credentials |
| 404 | Resource not found (image or file id) |
| 413 | The file exceeds 5 MB |
| 422 | Request validation error |
| 429 | Wallet quota exhausted |
| 500 | Processing failure; uses from that request are refunded |
Next steps
- Terminal guide: Process images with the API from the terminal (curl)
- Python guide: Process images with the SocialCutter API from Python
- Node guide: Process images with the SocialCutter API from Node
- API documentation: https://docs.socialcutter.theboomer.dev
- Dashboard: https://dash.socialcutter.theboomer.dev
Frequently asked questions
Do I need Composer?
Not if you use cURL, which ships with PHP. Guzzle is installed with Composer: composer require guzzlehttp/guzzle.
Where do I put the API key?
In the SOCIALCUTTER_API_KEY environment variable, read with getenv. It travels on every request in the X-API-Key header.
How do I upload a local file with cURL?
With CURLFile pointing at the file path and the file field, plus destinations as a JSON string. The limit is 5 MB.
How is a batch charged?
1 use per destination (platform and format) per image. A batch of 3 images with 2 destinations each is 6 uses.
Why do I get 429?
Because the wallet quota is exhausted. Check GET /api/v1/credits and GET /api/v1/wallet before large batches.