PHP & Laravel

The Transactional client API is a single POST. The built-in cURL extension is enough — no Composer package required. If you're on Laravel, the framework's HTTP client is even nicer.

Plain PHP (cURL extension)

<?php

function transactionalGenerate(string $documentId, array $variables): array
{
    $ch = curl_init('https://api.transactional.dev/v1/generate');
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => [
            'x-api-token: ' . getenv('TRANSACTIONAL_API_TOKEN'),
            'Content-Type: application/json',
        ],
        CURLOPT_POSTFIELDS => json_encode([
            'documentId' => $documentId,
            'variables'  => $variables,
        ]),
        CURLOPT_TIMEOUT => 30,
    ]);

    $body = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    $data = json_decode($body, true);
    if ($status < 200 || $status >= 300) {
        throw new RuntimeException(
            "Transactional {$status}: " . ($data['error'] ?? 'unknown') . ' — ' . ($data['message'] ?? '')
        );
    }
    return $data; // ['url' => '...', 'documentId' => '...']
}

$result = transactionalGenerate(
    '1d8e5d56-1a4f-4b62-8c33-2d34a64b2f00',
    [
        'customer' => ['name' => 'Acme Corp'],
        'invoice'  => ['number' => 'INV-2026-0142', 'total' => 1280.50],
    ]
);

echo $result['url'];

Laravel HTTP client

Laravel's HTTP client (built on Guzzle) gives you retries and timeouts in one line.

use Illuminate\Support\Facades\Http;

$response = Http::withHeaders([
    'x-api-token' => config('services.transactional.token'),
])
    ->timeout(30)
    ->retry(2, 200, throw: false) // 2 retries, 200ms baseline
    ->post('https://api.transactional.dev/v1/generate', [
        'documentId' => $documentId,
        'variables'  => $variables,
    ]);

if ($response->failed()) {
    $body = $response->json();
    throw new \RuntimeException(
        "Transactional {$response->status()}: "
        . ($body['error'] ?? 'unknown') . ' — '
        . ($body['message'] ?? '')
    );
}

return $response->json('url');

Add the token to config/services.php and pull it from .env:

// config/services.php
'transactional' => [
    'token' => env('TRANSACTIONAL_API_TOKEN'),
],

Branching on error codes

$body = $response->json();
$code = $body['error'] ?? null;

match (true) {
    $response->successful()                          => $body['url'],
    $code === 'quota_exceeded'                        => throw new OutOfCreditsException(),
    in_array($code, ['NOT_FOUND', 'invalid_document_id']) => throw new BadTemplateException($documentId),
    $code === 'UNAUTHORIZED'                          => throw new ApiTokenInvalidException(),
    default                                           => throw new RuntimeException('Transactional unknown error'),
};

Queueing as a job (Laravel)

Generating from a synchronous request blocks the HTTP cycle for a few hundred ms. Push it to a queue:

// app/Jobs/GenerateInvoicePdf.php
class GenerateInvoicePdf implements ShouldQueue
{
    use Queueable;

    public function __construct(
        public int $invoiceId,
        public string $documentUuid,
    ) {}

    public function handle(): void
    {
        $invoice = Invoice::findOrFail($this->invoiceId);

        $response = Http::withHeaders([
            'x-api-token' => config('services.transactional.token'),
        ])->retry(2, 200, throw: false)
          ->post('https://api.transactional.dev/v1/generate', [
              'documentId' => $this->documentUuid,
              'variables'  => $invoice->toTemplateVariables(),
          ]);

        if ($response->failed()) {
            // Let the queue retry policy handle it.
            $this->fail("Transactional failed: " . $response->status());
            return;
        }

        // Stream the PDF to S3.
        $pdf = Http::get($response->json('url'))->body();
        Storage::disk('s3')->put("invoices/{$invoice->id}.pdf", $pdf);

        $invoice->update(['pdf_path' => "invoices/{$invoice->id}.pdf"]);
    }
}

// Dispatch it
GenerateInvoicePdf::dispatch($invoice->id, $templateUuid);

Attaching the PDF to a Mailable

// app/Mail/InvoiceMail.php
public function attachments(): array
{
    $response = Http::withHeaders([
        'x-api-token' => config('services.transactional.token'),
    ])->post('https://api.transactional.dev/v1/generate', [
        'documentId' => config('invoices.template_uuid'),
        'variables'  => $this->invoice->toTemplateVariables(),
    ]);

    $pdf = Http::get($response->json('url'))->body();

    return [
        Attachment::fromData(fn () => $pdf, "invoice-{$this->invoice->number}.pdf")
            ->withMime('application/pdf'),
    ];
}

Next steps