Skip to main content
By Marcel Czuryszkiewicz, Founder @ bundle.social

TL;DR

  • No SDK needed. Native PHP cURL or any HTTP client (Guzzle, Laravel Http) works.
  • One API key, one base URL, 14+ platforms. Upload media, create posts, pull analytics, handle webhooks.
  • Every code example in this guide is verified against the actual API contracts. Copy-paste with confidence.

What You’ll Need

  • PHP 7.4+ (8.0+ recommended for typed properties)
  • cURL extension enabled (php -m | grep curl)
  • bundle.social API key - get one from the dashboard
No composer packages required. We’ll use PHP’s native cURL. If you prefer Guzzle or Laravel’s Http client, swap in the HTTP calls - the endpoints and payloads stay the same.

Setup: The Client Class

Here’s a minimal client that handles auth, JSON encoding, multipart uploads, and proper error surfacing:
<?php

class BundleSocialClient
{
    private string $apiKey;
    private string $baseUrl = 'https://api.bundle.social/api/v1';

    public function __construct(string $apiKey)
    {
        $this->apiKey = $apiKey;
    }

    public function get(string $endpoint, array $params = []): array
    {
        $query = !empty($params) ? '?' . http_build_query($params) : '';
        return $this->request('GET', $endpoint . $query);
    }

    public function post(string $endpoint, array $data): array
    {
        return $this->request('POST', $endpoint, $data);
    }

    public function patch(string $endpoint, array $data): array
    {
        return $this->request('PATCH', $endpoint, $data);
    }

    public function delete(string $endpoint): array
    {
        return $this->request('DELETE', $endpoint);
    }

    public function uploadFile(string $filePath, string $teamId): array
    {
        $data = [
            'file'   => new CURLFile($filePath),
            'teamId' => $teamId,
        ];
        return $this->request('POST', '/upload', $data, true);
    }

    private function request(
        string $method,
        string $endpoint,
        ?array $data = null,
        bool $isMultipart = false
    ): array {
        $ch = curl_init();
        $url = $this->baseUrl . $endpoint;

        $headers = ['x-api-key: ' . $this->apiKey];
        if (!$isMultipart) {
            $headers[] = 'Content-Type: application/json';
        }

        curl_setopt_array($ch, [
            CURLOPT_URL            => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER     => $headers,
            CURLOPT_CUSTOMREQUEST  => $method,
        ]);

        if ($data !== null) {
            curl_setopt(
                $ch,
                CURLOPT_POSTFIELDS,
                $isMultipart ? $data : json_encode($data)
            );
        }

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $curlError = curl_error($ch);
        curl_close($ch);

        if ($curlError) {
            throw new RuntimeException("cURL error: {$curlError}");
        }

        $result = json_decode($response, true);

        if ($httpCode === 429) {
            throw new RuntimeException(
                'Rate limited. Back off and retry with exponential backoff.'
            );
        }

        if ($httpCode >= 400) {
            throw new BundleSocialException(
                $result['message'] ?? 'Unknown API error',
                $httpCode,
                $result['errorsVerbose'] ?? null
            );
        }

        return $result;
    }
}

class BundleSocialException extends RuntimeException
{
    private ?array $errorsVerbose;

    public function __construct(
        string $message,
        int $code,
        ?array $errorsVerbose = null
    ) {
        parent::__construct($message, $code);
        $this->errorsVerbose = $errorsVerbose;
    }

    /** Platform-specific error details (null for non-post errors) */
    public function getErrorsVerbose(): ?array
    {
        return $this->errorsVerbose;
    }
}
Usage:
$client = new BundleSocialClient('pk_live_your_api_key_here');
API keys are org-scoped. One key gives access to all teams under your organization. Store it in environment variables, never hardcode it. See API key docs for details.

Teams: Create Workspaces

Teams are isolated workspaces - each team has its own social accounts, posts, and uploads. Think of them as your “client” or “workspace.”

Create a Team

$team = $client->post('/team', [
    'name' => 'Acme Corp Marketing',
]);

echo "Team ID: " . $team['id'] . "\n";
// Save this ID - you'll need it for everything else

List Teams

$teams = $client->get('/team', [
    'limit'  => 20,
    'offset' => 0,
    'search' => 'Acme',   // optional search filter
]);

foreach ($teams['items'] as $team) {
    echo $team['name'] . " (ID: " . $team['id'] . ")\n";
}
One Organization, many Teams. Don’t create separate bundle.social accounts per client. Create Teams programmatically. All billing flows through your single subscription. For the full architecture breakdown, see our Multi-Tenant Architecture Guide.

Connect Social Accounts

Instead of building OAuth flows for 14+ platforms (please don’t), use our hosted portal:
$portal = $client->post('/social-account/create-portal-link', [
    'teamId'              => $teamId,
    'redirectUrl'         => 'https://yourapp.com/dashboard',
    'socialAccountTypes'  => ['INSTAGRAM', 'TIKTOK', 'LINKEDIN', 'YOUTUBE'],
    // White label it
    'logoUrl'             => 'https://yourapp.com/logo.png',
    'hidePoweredBy'       => true,
    'language'            => 'en',   // supports: en, pl, fr, de, es, it, nl, pt, ru, tr, zh, hi, sv
]);

// Redirect your user to this URL
header('Location: ' . $portal['url']);
exit;
The user completes OAuth on our branded page (with YOUR logo), then gets redirected back to your redirectUrl. We handle token storage, refresh, and permission scopes.
Pro tip: For YouTube, Facebook, Instagram, LinkedIn, and Google Business, users also need to select a channel/page after OAuth. The hosted portal handles this automatically. See the Connect Social Accounts docs for the full flow.

Upload Media

Two methods. Pick based on file size.

Simple Upload (Images & Small Videos)

Good for anything under 90 MB. Standard multipart/form-data:
$upload = $client->uploadFile('/path/to/image.jpg', $teamId);

echo "Upload ID: " . $upload['id'] . "\n";
echo "Type: "      . $upload['type'] . "\n";      // "image" or "video"
echo "Size: "      . $upload['fileSize'] . " bytes\n";

Resumable Upload (Large Videos)

For big files. Three steps: init, push bytes, finalize.
// Step 1: Initialize - tell us what's coming
$init = $client->post('/upload/init', [
    'teamId'   => $teamId,
    'fileName' => 'big-video.mp4',
    'mimeType' => 'video/mp4',   // video/mp4, image/jpg, image/jpeg, image/png, application/pdf
]);

$uploadUrl = $init['url'];    // Pre-signed URL (expires in 10 minutes!)
$path      = $init['path'];   // Keep this for Step 3

// Step 2: Push bytes to the signed URL (raw PUT, no auth headers needed)
$filePath   = '/path/to/big-video.mp4';
$fileHandle = fopen($filePath, 'r');
$fileSize   = filesize($filePath);

$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL            => $uploadUrl,
    CURLOPT_PUT            => true,
    CURLOPT_INFILE         => $fileHandle,
    CURLOPT_INFILESIZE     => $fileSize,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Content-Type: video/mp4'],
]);
curl_exec($ch);
curl_close($ch);
fclose($fileHandle);

// Step 3: Finalize - register the file in our system
$upload = $client->post('/upload/finalize', [
    'teamId' => $teamId,
    'path'   => $path,
]);

echo "Upload ID: " . $upload['id'] . "\n";
The pre-signed URL expires after 10 minutes. Don’t go make coffee between Step 1 and Step 2. If you do, just re-initialize. Also: use resumable for any video. If a 1 GB upload fails at 99% with the simple method, you start over. With resumable, you don’t.
For supported formats and per-platform size limits, see Platform Limits.

Create Posts

Every post needs a teamId, a title, a postDate (ISO 8601), a status, the target platforms, and platform-specific data.

Single Platform: Instagram Reel

$post = $client->post('/post', [
    'teamId'             => $teamId,
    'title'              => 'Behind the scenes',
    'postDate'           => date('c'),           // ISO 8601, current time = post now
    'status'             => 'SCHEDULED',
    'socialAccountTypes' => ['INSTAGRAM'],
    'data'               => [
        'INSTAGRAM' => [
            'type'        => 'REEL',             // POST, REEL, or STORY
            'text'        => 'Behind the scenes footage #bts #reels',  // max 2000 chars
            'uploadIds'   => [$upload['id']],
            'shareToFeed' => true,               // show Reel in feed grid
        ],
    ],
]);

echo "Post ID: " . $post['id'] . "\n";
echo "Status: "  . $post['status'] . "\n";

Multiple Platforms at Once

Each platform gets its own data block with platform-specific fields:
$post = $client->post('/post', [
    'teamId'             => $teamId,
    'title'              => 'New product launch',
    'postDate'           => '2026-03-01T10:00:00Z',   // schedule for later
    'status'             => 'SCHEDULED',
    'socialAccountTypes' => ['INSTAGRAM', 'TIKTOK', 'YOUTUBE', 'LINKEDIN'],
    'data'               => [
        'INSTAGRAM' => [
            'type'      => 'REEL',
            'text'      => 'It\'s here. #launch #newproduct',
            'uploadIds' => [$uploadId],
        ],
        'TIKTOK' => [
            'text'      => 'We made a thing. #fyp #launch',   // max 2200 chars
            'uploadIds' => [$uploadId],
            'privacy'   => 'PUBLIC_TO_EVERYONE',
            // Also available: SELF_ONLY, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR
        ],
        'YOUTUBE' => [
            'type'        => 'SHORT',            // SHORT or VIDEO
            'text'        => 'New Product Launch', // this is the video TITLE (max 100 chars)
            'description' => 'Check out our latest product.', // max 5000 chars
            'uploadIds'   => [$uploadId],
            'privacy'     => 'PUBLIC',           // PUBLIC, PRIVATE, or UNLISTED
            'madeForKids' => false,
        ],
        'LINKEDIN' => [
            'text'      => 'Excited to announce our latest launch. Full details in the comments.',
            // text is REQUIRED for LinkedIn (max 3000 chars)
            'uploadIds' => [$uploadId],
            'privacy'   => 'PUBLIC',             // PUBLIC, CONNECTIONS, LOGGED_IN, CONTAINER
        ],
    ],
]);

Text-Only Posts (No Media)

$post = $client->post('/post', [
    'teamId'             => $teamId,
    'title'              => 'Quick update',
    'postDate'           => date('c'),
    'status'             => 'SCHEDULED',
    'socialAccountTypes' => ['TWITTER', 'LINKEDIN'],
    'data'               => [
        'TWITTER' => [
            'text' => 'We just shipped v2.0. That is all.',  // max 280 chars (25K for Premium)
        ],
        'LINKEDIN' => [
            'text' => 'We just shipped v2.0. Here\'s what changed and why it matters for your workflow.',
        ],
    ],
]);

Facebook: Posts, Reels & Stories

// Facebook page post with a link
$post = $client->post('/post', [
    'teamId'             => $teamId,
    'title'              => 'Blog share',
    'postDate'           => date('c'),
    'status'             => 'SCHEDULED',
    'socialAccountTypes' => ['FACEBOOK'],
    'data'               => [
        'FACEBOOK' => [
            'type' => 'POST',                    // POST, REEL, or STORY
            'text' => 'New on the blog: How we reduced video upload failures by 80%.',
            'link' => 'https://yoursite.com/blog/video-uploads',  // only for type POST
        ],
    ],
]);
Platform-specific fields vary. Instagram has collaborators and tagged users. TikTok has disableComments, disableDuet, disableStitch, isAiGenerated. Pinterest requires a boardId. Check the Platform Guides for every field per platform.

Post Status Values

StatusWhen to use
SCHEDULEDWith a future postDate - publishes at that time
SCHEDULEDWith current postDate (date('c')) - publishes immediately
DRAFTSave without publishing. Edit and schedule later
After publishing, posts transition to POSTED, ERROR, or PROCESSING. You can’t set these - they’re system-managed. Listen for post.published and post.failed webhooks to track status.

List & Manage Posts

List Posts

$posts = $client->get('/post', [
    'teamId' => $teamId,             // required
    'status' => 'SCHEDULED',         // DRAFT, SCHEDULED, POSTED, ERROR, PROCESSING, REVIEW, RETRYING
    'limit'  => 20,
    'offset' => 0,
    'order'  => 'DESC',              // ASC or DESC
    'orderBy' => 'postDate',         // createdAt, updatedAt, postDate, postedDate
]);

echo "Total: " . $posts['total'] . "\n";

foreach ($posts['items'] as $post) {
    echo $post['title'] . " - " . $post['status'] . " - " . $post['postDate'] . "\n";
}

Delete a Post

$client->delete('/post/' . $postId);

Retry a Failed Post

$client->post('/post/' . $postId . '/retry', []);

Analytics

Analytics require a teamId and platformType. The response contains a items array - each item is a daily analytics snapshot (refreshed every 24h, retained for 40 days).
Analytics are available on paid tiers only (PRO and BUSINESS). Not available for Twitter/X, Discord, or Slack. See the Analytics docs for per-platform availability.

Social Account Analytics

$analytics = $client->get('/analytics/social-account', [
    'teamId'       => $teamId,
    'platformType' => 'INSTAGRAM',
    // Valid: INSTAGRAM, FACEBOOK, LINKEDIN, TIKTOK, YOUTUBE,
    //        THREADS, PINTEREST, REDDIT, MASTODON, BLUESKY, GOOGLE_BUSINESS
]);

// $analytics['socialAccount'] - the social account object
// $analytics['items']         - array of analytics snapshots (newest last)

$latest = end($analytics['items']);

echo "Followers: "   . $latest['followers']   . "\n";
echo "Impressions: " . $latest['impressions'] . "\n";
echo "Views: "       . $latest['views']       . "\n";
echo "Likes: "       . $latest['likes']       . "\n";
echo "Comments: "    . $latest['comments']    . "\n";
echo "Post Count: "  . $latest['postCount']   . "\n";

Post Analytics

$postAnalytics = $client->get('/analytics/post', [
    'postId'       => $postId,
    'platformType' => 'INSTAGRAM',
]);

// $postAnalytics['post']  - the post object
// $postAnalytics['items'] - array of analytics snapshots

$latest = end($postAnalytics['items']);

echo "Impressions: " . $latest['impressions'] . "\n";
echo "Likes: "       . $latest['likes']       . "\n";
echo "Comments: "    . $latest['comments']    . "\n";
echo "Shares: "      . $latest['shares']      . "\n";
echo "Saves: "       . $latest['saves']       . "\n";
echo "Views: "       . $latest['views']       . "\n";

Force Refresh Analytics

// Rate limited to (number of teams × 5) per day
$client->post('/analytics/social-account/force', [
    'teamId'       => $teamId,
    'platformType' => 'INSTAGRAM',
]);
Some metrics return 0. This doesn’t mean zero engagement - it means the platform API doesn’t provide that data point. For example, Twitter/X has no analytics API at all, and YouTube doesn’t expose follower counts via API. Each Platform Guide documents exactly which fields return 0 and why.

Webhooks: Signature Verification

Webhooks fire at the organization level. All events from all teams hit the same endpoints. Always verify the signature - we send an HMAC-SHA256 hash in the x-signature header.

Plain PHP

<?php

// Get raw body BEFORE any parsing
$body      = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
$secret    = getenv('BUNDLE_WEBHOOK_SECRET'); // your webhook secret

// Verify HMAC-SHA256 signature
$expected = hash_hmac('sha256', $body, $secret);

if (!hash_equals($expected, $signature)) {
    http_response_code(401);
    echo json_encode(['error' => 'Invalid signature']);
    exit;
}

// Signature valid - process the event
$event = json_decode($body, true);

switch ($event['type']) {
    case 'post.published':
        // Post went live
        $postId   = $event['data']['id'];
        $teamId   = $event['data']['teamId'];
        $status   = $event['data']['status'];  // "POSTED"
        // Update your database, notify the user
        break;

    case 'post.failed':
        // Post failed after retries
        $postId = $event['data']['id'];
        // Alert the user, log error details
        break;

    case 'social-account.created':
        // New social account connected
        $accountType = $event['data']['type'];     // "INSTAGRAM", "TIKTOK", etc.
        $teamId      = $event['data']['teamId'];
        $username    = $event['data']['username'];
        // Update available platforms in your UI
        break;

    case 'social-account.deleted':
        // Account disconnected - remove from your UI
        break;

    case 'team.created':
    case 'team.updated':
        // team.updated also fires when social accounts are added/removed
        break;

    case 'team.deleted':
        // Clean up your database
        break;

    case 'comment.published':
        // Auto-comment posted
        break;
}

http_response_code(200);
echo 'ok';

Webhook Delivery Details

SettingValue
Timeout15 seconds per delivery
Max attempts3 (initial + 2 retries)
BackoffExponential, starting at 30s
Auto-disableAfter 50 consecutive failures in 24h
Respond fast. Return a 200 within 15 seconds. Do heavy processing asynchronously - push to a queue (Redis, RabbitMQ, database job table). If your handler is slow, we count it as a failure. 50 consecutive failures and we auto-disable your webhook. See the Webhooks docs for full payload examples.

Error Handling

When a post fails on specific platforms, the response includes errorsVerbose - a per-platform breakdown of what went wrong:
try {
    $post = $client->post('/post', $postData);
    echo "Post created: " . $post['id'] . "\n";
} catch (BundleSocialException $e) {
    echo "Error: " . $e->getMessage() . "\n";
    echo "HTTP Code: " . $e->getCode() . "\n";

    $verbose = $e->getErrorsVerbose();
    if ($verbose) {
        // Each platform has its own error entry
        foreach ($verbose as $platform => $error) {
            if ($error === null) {
                echo "{$platform}: Success\n";
                continue;
            }

            echo "{$platform}: {$error['userFacingMessage']}\n";
            echo "  Code: {$error['code']}\n";           // e.g. "META:190", "TT:spam_risk"
            echo "  Transient: " . ($error['isTransient'] ? 'yes (retry)' : 'no (fix it)') . "\n";
            echo "  Raw: {$error['errorMessage']}\n";     // upstream platform error
        }
    }
}

Error Code Prefixes

PrefixPlatform
METAInstagram, Facebook, Threads
TTTikTok
LILinkedIn
YTYouTube
HTTPGeneric API errors

The isTransient Field

ValueMeaningAction
trueRate limit, temporary outage, timeoutRetry with exponential backoff
falseAuth error, content rejected, validationFix the input or reconnect the account
For the full error reference, see the Errors docs.

Rate Limiting

Two layers. Both matter.

Layer 1: API Rate Limits

LayerWindowMax Requests
Burst1 second100
Short10 seconds500
Minute1 minute2,000
Tracked per API key. Hit any limit → 429 Too Many Requests. Implement exponential backoff:
function requestWithRetry(BundleSocialClient $client, string $endpoint, array $data, int $maxRetries = 3): array
{
    for ($attempt = 0; $attempt <= $maxRetries; $attempt++) {
        try {
            return $client->post($endpoint, $data);
        } catch (RuntimeException $e) {
            if ($e->getCode() === 429 && $attempt < $maxRetries) {
                $wait = pow(2, $attempt) + random_int(0, 1000) / 1000; // jitter
                sleep((int) $wait);
                continue;
            }
            throw $e;
        }
    }
}

Layer 2: Platform Posting Limits

Daily caps per social account per platform (varies by subscription tier):
PlatformFREEPROBUSINESS
Instagram10/day20/day25/day
TikTok5/day10/day15/day
Twitter/X5/day15/day15/day
YouTube10/day10/day15/day
Plus monthly org-wide caps: FREE = 10, PRO = 1,000, BUSINESS = 100,000.

Full Example: Laravel Integration

Service Class

<?php
// app/Services/BundleSocialService.php

namespace App\Services;

use Illuminate\Support\Facades\Http;
use Illuminate\Http\Client\Response;

class BundleSocialService
{
    private string $baseUrl = 'https://api.bundle.social/api/v1';

    private function headers(): array
    {
        return ['x-api-key' => config('services.bundlesocial.api_key')];
    }

    public function createTeam(string $name): array
    {
        return Http::withHeaders($this->headers())
            ->post("{$this->baseUrl}/team", ['name' => $name])
            ->throw()
            ->json();
    }

    public function uploadMedia(string $filePath, string $teamId): array
    {
        return Http::withHeaders($this->headers())
            ->attach('file', file_get_contents($filePath), basename($filePath))
            ->post("{$this->baseUrl}/upload", ['teamId' => $teamId])
            ->throw()
            ->json();
    }

    public function createPost(array $data): array
    {
        return Http::withHeaders($this->headers())
            ->post("{$this->baseUrl}/post", $data)
            ->throw()
            ->json();
    }

    public function getAnalytics(string $teamId, string $platform): array
    {
        return Http::withHeaders($this->headers())
            ->get("{$this->baseUrl}/analytics/social-account", [
                'teamId'       => $teamId,
                'platformType' => $platform,
            ])
            ->throw()
            ->json();
    }

    public function getPortalLink(string $teamId, array $platforms): string
    {
        $response = Http::withHeaders($this->headers())
            ->post("{$this->baseUrl}/social-account/create-portal-link", [
                'teamId'             => $teamId,
                'redirectUrl'        => route('dashboard'),
                'socialAccountTypes' => $platforms,
                'hidePoweredBy'      => true,
            ])
            ->throw()
            ->json();

        return $response['url'];
    }
}

Webhook Controller

<?php
// app/Http/Controllers/WebhookController.php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Http\Response;

class WebhookController extends Controller
{
    public function handle(Request $request): Response
    {
        // Verify signature
        $signature = $request->header('x-signature', '');
        $expected  = hash_hmac(
            'sha256',
            $request->getContent(),
            config('services.bundlesocial.webhook_secret')
        );

        if (!hash_equals($expected, $signature)) {
            abort(401, 'Invalid signature');
        }

        $event = $request->json()->all();

        match ($event['type']) {
            'post.published'          => $this->handlePostPublished($event['data']),
            'post.failed'             => $this->handlePostFailed($event['data']),
            'social-account.created'  => $this->handleAccountCreated($event['data']),
            'social-account.deleted'  => $this->handleAccountDeleted($event['data']),
            default                   => null,
        };

        return response('ok', 200);
    }

    private function handlePostPublished(array $data): void
    {
        // Update your DB, notify the user
        // $data['id'], $data['teamId'], $data['status'], $data['postedDate']
    }

    private function handlePostFailed(array $data): void
    {
        // Alert user, log failure details
    }

    private function handleAccountCreated(array $data): void
    {
        // $data['type'] = "INSTAGRAM", "TIKTOK", etc.
        // $data['teamId'], $data['username']
    }

    private function handleAccountDeleted(array $data): void
    {
        // Remove from your UI
    }
}
Laravel route tip: Register the webhook route outside the web middleware group (no CSRF verification needed). Add it in routes/api.php or exclude it from CSRF in your middleware.

Config

// config/services.php
'bundlesocial' => [
    'api_key'        => env('BUNDLESOCIAL_API_KEY'),
    'webhook_secret' => env('BUNDLESOCIAL_WEBHOOK_SECRET'),
],

Platform-Specific Fields Reference

Quick reference for the most commonly used platform-specific fields. For the full spec, see Platform Guides.
PlatformKey FieldsNotes
Instagramtype (POST/REEL/STORY), text, uploadIds, shareToFeed, collaborators, taggedtext max 2000 chars. collaborators max 3 usernames
TikToktype (VIDEO/IMAGE), text, uploadIds, privacy, disableComments, isAiGeneratedtext max 2200 chars. IMAGE type: JPG only
YouTubetype (SHORT/VIDEO), text (title!), description, uploadIds, privacy, madeForKidstext is the video TITLE (max 100). description max 5000
LinkedIntext (required!), uploadIds, privacy, mediaTitle, hideFromFeedtext max 3000 chars. Supports PDF documents
Twitter/Xtext, uploadIds280 chars (Free/Basic), 25K chars (Premium)
Facebooktype (POST/REEL/STORY), text, uploadIds, linklink only for type POST. text max 50K chars
Pinteresttext, description, uploadIds, boardName (required!), linktext max 100 chars. boardName from socialAccount.channels
Redditsr (required!), text, uploadIds, flairId, link, nsfwsr format: r/subredditName or u/username. text max 300 chars

Resources

Swagger / OpenAPI Spec

Interactive API explorer - test endpoints directly in your browser

Questions? Running into edge cases with PHP specifically? Reach out - we’ve seen enough curl_setopt configurations to last a lifetime.