Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Cloudconvert Php Laravel Package

cloudconvert/cloudconvert-php

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire up config

composer require cloudconvert/cloudconvert-php

The SDK talks to the CloudConvert API v2 (SDK ^2.x). Everything you do is a Job containing ordered Tasks (import/*, convert, export/*, etc.).

Add credentials to config/services.php (never read env() outside config — config caching will bite you):

'cloudconvert' => [
    'api_key'         => env('CLOUDFONVERT_API_KEY'), // CLOUDCONVERT_API_KEY
    'sandbox'         => env('CLOUDCONVERT_SANDBOX', false),
    'webhook_secret'  => env('CLOUDCONVERT_WEBHOOK_SECRET'),
],

Bind the client as a singleton in AppServiceProvider::register() so you don't rebuild Guzzle per resolution:

$this->app->singleton(\CloudConvert\CloudConvert::class, fn () => new \CloudConvert\CloudConvert([
    'api_key' => config('services.cloudconvert.api_key'),
    'sandbox' => (bool) config('services.cloudconvert.sandbox'),
]));

Where to look first

  1. CloudConvert\Models\Task — the generic task object; the real documentation is the operations reference (every set() key, e.g. output_format, engine, engine_version, filename, pages).
  2. The resource accessors on the client: jobs(), tasks(), users(), webhooks(). Most days you only touch jobs() and tasks().
  3. The sandbox mode — flip sandbox => true and you can iterate without burning credits.

First use case: sanity check, then a DOCX → PDF pipeline

// 1. Does auth + billing work at all?
$me = $cloudConvert->users()->me();
dump($me->getCredits()); // fail fast if this throws 401

// 2. Build a job
$job = (new Job())
    ->addTask(new Task('import/upload', 'upload'))
    ->addTask(
        (new Task('convert', 'convert'))
            ->set('input', ['upload'])
            ->set('output_format', 'pdf')
    )
    ->addTask(
        (new Task('export/url', 'export'))
            ->set('input', ['convert'])
    );

$cloudConvert->jobs()->create($job);

// 3. Push the bytes into the upload task
$cloudConvert->tasks()->upload(
    $job->getTasks()->whereName('upload')[0],
    fopen(storage_path('app/in.docx'), 'r'),
    'in.docx'
);

// 4. Block until done (fine for a CLI/tinker smoke test only)
$cloudConvert->jobs()->wait($job);

$file = $job->getTasks()->whereName('export')[0]->getResult()->files[0];
dump($file->url, $file->filename); // temporary signed download URL

If that returns a URL you can curl, you have the whole mental model: create job → import → transform → export → consume URL.

Implementation Patterns

The three import flavours — pick deliberately

Task Use when
import/upload Local file / Laravel Storage stream. Two-step: create job, then tasks()->upload().
import/url The file is already publicly fetchable. ->set('url', $signedUrl).
import/s3 (or signed URL into import/url) Private S3. CloudConvert cannot read your private bucket; generate a short-lived signed URL, or use the SDK's signed-URL resource for direct-to-CloudConvert uploads.

Wrap it in a queued job (the default for production)

jobs()->wait() polls, so never call it from a controller on a large file. Queue it, set $timeout generously, and let Horizon/Redis handle retries.

class ConvertDocumentToPdf implements ShouldQueue
{
    public int $tries = 2;
    public int $timeout = 300;

    public function __construct(public int $documentId) {}

    public function handle(CloudConvert $cloudConvert): void
    {
        $document = Document::findOrFail($this->documentId);
        $name = 'doc-'.$document->id;

        $job = (new Job())
            ->addTask(new Task('import/upload', 'upload'))
            ->addTask((new Task('convert', 'convert'))
                ->set('input', ['upload'])
                ->set('output_format', 'pdf')
                ->set('engine', 'office')
                ->set('filename', $name.'.pdf'))
            ->addTask((new Task('export/url', 'export'))
                ->set('input', ['convert']));

        $cloudConvert->jobs()->create($job);

        $uploadTask = $job->getTasks()->whereName('upload')[0];
        $cloudConvert->tasks()->upload(
            $uploadTask,
            Storage::disk('local')->readStream($document->path),
            basename($document->path)
        );

        $cloudConvert->jobs()->wait($job, 240);

        if ($job->getStatus() !== 'finished') {
            $errors = $job->getTasks()->whereStatus('error')->all();
            throw new RuntimeException('CloudConvert job failed: '.json_encode($errors));
        }

        $file = $job->getTasks()->whereName('export')[0]->getResult()->files[0];

        // Download NOW — result URLs are temporary.
        $stream = fopen('php://temp', 'w+');
        $client = new \GuzzleHttp\Client();
        $client->get($file->url, ['sink' => $stream]);
        rewind($stream);

        Storage::disk('s3')->writeStream(
            "converted/{$name}.pdf", $stream
        );

        $document->update(['pdf_path' => "converted/{$name}.pdf"]);
    }
}

Dispatch it from a DocumentObserver/created hook and the rest of the app stays synchronous-looking.

Webhook-driven flow (what CloudConvert actually recommends)

For user-facing flows, don't hold a worker for 60s polling. Create the job with ->set('tag', "document:{$id}"), return immediately, and let CloudConvert call you back:

Route::post('/webhooks/cloudconvert', CloudConvertWebhookController::class)
    ->withoutMiddleware(['csrf']); // or exclude in bootstrap/app.php
public function __invoke(Request $request)
{
    $ts  = $request->header('CloudConvert-Timestamp');
    $sig = $request->header('CloudConvert-Signature');

    $expected = hash_hmac(
        'sha256',
        $ts.'.'.$request->getContent(),
        config('services.cloudconvert.webhook_secret')
    );

    abort_unless($sig && hash_equals($expected, $sig), 403);

    $payload = $request->json()->all();
    if ($payload['event'] !== 'job.finished') {
        return response()->noContent();
    }

    $job = app(CloudConvert::class)->jobs()->get($payload['job']['id']);

    // Re-derive your entity from the tag you set at creation time
    [$_, $documentId] = explode(':', $job->getTag());
    DownloadConvertedDocument::dispatch((int) $documentId, $job->getId());

    return response()->noContent();
}

Register the endpoint once with $cloudConvert->webhooks()->create(...), and make the handler idempotent (CloudConvert retries; store job_id uniquely).

Task naming and composition

  • Task names must be unique within a job; input arrays reference names.
  • Use $job->getTasks()->whereName('convert') rather than positional indexes — collection order is not a contract.
  • Fan-in is supported: pass ->set('input', ['pdf-1', 'pdf-2']) for a merge task; fan-out for split + archive.
  • Reuse a small factory so pipeline shape lives in one place:
class Pipeline
{
    public static function convertTo(string $format, string $from = 'upload'): Job
    {
        return (new Job())
            ->addTask(new Task('import/upload', 'upload'))
            ->addTask((new Task('convert', 'convert'))
                ->set('input', [$from])
                ->set('output_format', $format))
            ->addTask((new Task('export/url', 'export'))
                ->set('input', ['convert']));
    }
}

Testing

The client is Guzzle-based, so inject a MockHandler and assert on request bodies — no network, no credits, no sandbox dependency:

$mock = new \GuzzleHttp\Handler\MockHandler([
    new \GuzzleHttp\Psr7\Response(200, [], json_encode(['data' => $fakeJob])),
]);
$cloudConvert = new CloudConvert([
    'api_key' => 'test',
    'guzzle_options' => ['handler' => \GuzzleHttp\HandlerStack::create($mock)],
]);

For the queued job, assert on the resulting Document state rather than the SDK calls where possible — the interesting logic is stream handling and error mapping, not the HTTP.

Gotchas and Tips

  • Result URLs expire. They are signed, short-lived URLs. Download and re-store (S3/local) inside the same job that produced them; persisting $file->url in your DB is a classic production bug.
  • Credits are the failure mode you'll actually hit. users()->me()->getCredits() on a health page or pre-flight check beats a mysterious 402 mid-job. Also handle "job finished but one task errored" — $job->getStatus() is the authority, and whereStatus('error')->all() gives you the per-task message.
  • wait() blocks and polls. Give it an explicit timeout (the default waits indefinitely). For anything a user is staring at, use webhooks; for batch/back-office, queue + wait() is simpler and fine.
  • Sandbox doesn't test your file access. Sandbox mode is great for pipeline wiring but your import/url sources still need to be reachable by CloudConvert's servers. Private S3 objects need a signed URL — the most common "it works in tinker, fails in prod" cause.
  • Pin engine_version when output fidelity matters. CloudConvert upgrades engines (LibreOffice, ImageMagick, etc.); conversions that were pixel-perfect last quarter can shift. Set ->set('engine', 'office')->set('engine_version', '7.16.0') (or whatever the operations page lists) for anything contractual.
  • set() is unvalidated magic. Typos like output_format → outputformat don't fail client-side; they surface as a 400 from the API or a silently wrong job. Keep option keys in the pipeline factory, not scattered through controllers.
  • Large files: stream, don't file_get_contents. Use Storage::readStream() for upload and Guzzle's 'sink' => $stream for download, then writeStream() to S3. A 200 MB PDF read into a PHP string will blow memory_limit inside a worker.
  • Webhook route needs CSRF exemption, must be POST, must not be behind auth, and must return 2xx quickly — dispatch the heavy lifting (including fetching the job and downloading the output) to a queue. Verify the HMAC signature exactly as shown; the signed payload is timestamp + "." + rawBody, so use getContent(), not json_encode($request->all()).
  • Tag your jobs. ->set('tag', "document:{$id}") (≤ 255 chars) is the cleanest way to correlate the webhook back to your model without a lookup table.
  • Don't expose the API key to the browser. If you need client-side direct upload, mint a short-lived signed URL server-side and hand only that to the frontend.
  • Extension points are thin on purpose. There's no Laravel service provider or facade shipped — you write ~10 lines yourself and, in exchange, get an SDK that tracks the API closely. Wrap it in your own DocumentConverter service so the SDK types (Job, Task) don't leak into controllers; that also gives you a single seam for the Guzzle mock in tests.
  • Version discipline: cloudconvert-php v1 targets the old v1 API and its method surface (convert(), etc.) is unrelated — if you find a tutorial using $api->convert([...]), it's v1 and will not compile against ^2.x.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo