trydoku / php-sdk
Official PHP client for the TRYDOKU document generation API
Requires
- php: ^8.2
- ext-json: *
- php-http/discovery: ^1.19
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
- psr/http-message: ^1.1|^2.0
Requires (Dev)
- guzzlehttp/guzzle: ^7.0
- guzzlehttp/psr7: ^2.0
- infection/infection: ^0.29
- php-cs-fixer/shim: ^3.0
- php-http/mock-client: ^1.6
- phpunit/phpunit: ^11.0
Suggests
- guzzlehttp/guzzle: PSR-18 HTTP client for Laravel and standalone PHP projects (^7.0)
- nyholm/psr7: Lightweight PSR-17 message factory, pairs well with symfony/http-client (^1.8)
- symfony/http-client: PSR-18 HTTP client for Symfony projects (^6.4|^7.0)
Provides
None
Conflicts
None
Replaces
None
README
Official PHP client for the TRYDOKU document generation API.
Fill a Word template with structured data and download the results as .docx or PDF files — invoices, contracts, reports, and similar documents — from PHP.
Table of contents
- Requirements
- Installation
- Get an API token
- Quick start
- Usage
- Error handling
- Development
- Contributing
- Security
- License
- Support
Requirements
- PHP 8.2 or newer
- A PSR-18 HTTP client (for example Guzzle or Symfony HttpClient)
- A PSR-17 HTTP factory (for example
guzzlehttp/psr7ornyholm/psr7)
The SDK talks to HTTP through those interfaces. It does not bundle an HTTP client, so you pick the one that already fits your app.
Installation
composer require trydoku/php-sdk
Then install an HTTP client if your project does not already have one.
Laravel or standalone PHP:
composer require guzzlehttp/guzzle guzzlehttp/psr7
Symfony:
composer require symfony/http-client nyholm/psr7
If Composer asks to allow the php-http/discovery plugin, accept it. The SDK uses that plugin to find the HTTP client you installed.
Get an API token
- Sign in at trydoku.com.
- Open Account Settings → API Keys.
- Create a personal access token.
Keep the token out of git, logs, and frontend code. Changing your account password revokes every existing token.
Quick start
<?php require 'vendor/autoload.php'; use Trydoku\Client; $client = new Client(getenv('TRYDOKU_API_TOKEN')); // 1. Start a batch from a template stored in TRYDOKU $batch = $client->documents()->generate( templateUuid: 'e81d77a2-f674-4b53-a8ee-bf350284e311', data: [ [ 'Client_Name' => 'Acme Corporation', 'Invoice_Number' => 'INV-2026-001', 'Amount_Due' => '1250.00', ], [ 'Client_Name' => 'Globex Industries', 'Invoice_Number' => 'INV-2026-002', 'Amount_Due' => '3400.00', ], ], ); echo "Batch {$batch->id} is {$batch->status}\n"; // 2. Generation is asynchronous — poll until the batch finishes $batch = $client->batches()->waitForCompletion($batch->id); if (!$batch->isCompleted() || $batch->hasFailed()) { throw new RuntimeException( "Generation finished with status {$batch->status} ({$batch->failedItems} failed rows)." ); } // 3. Download every completed document as a single ZIP file_put_contents('documents.zip', $client->batches()->downloadZip($batch->id)); echo "Saved documents.zip ({$batch->processedItems}/{$batch->totalItems} rows)\n";
Each item in data becomes one document. Placeholder names in the template (Client_Name, and so on) must match the keys you send, unless you add a variable mapping.
Usage
Create a client
use Trydoku\Client; use Trydoku\Config; // Token only — the default production URL is used $client = new Client('YOUR_API_TOKEN'); // Custom base URL (staging or a reverse proxy) $client = new Client(new Config( apiToken: 'YOUR_API_TOKEN', baseUrl: 'https://www.trydoku.com/api/v1', )); // Your own PSR-18 client, with timeouts configured there $http = new GuzzleHttp\Client(['timeout' => 60]); $client = new Client('YOUR_API_TOKEN', httpClient: $http);
Timeouts, retries, and proxies belong on the HTTP client you pass in. The SDK does not take a timeout option of its own.
Generate from a stored template
$batch = $client->documents()->generate( templateUuid: 'e81d77a2-f674-4b53-a8ee-bf350284e311', data: [ ['Client_Name' => 'Acme Corp', 'Amount' => '1250.00'], ], );
Pass either templateUuid or templateBase64, never both, and never neither. A batch accepts 1–500 rows. The SDK throws InvalidArgumentException before it sends the request if those rules are broken.
Generate from a local .docx file
$batch = $client->documents()->generate( templateBase64: base64_encode(file_get_contents('template.docx')), data: [ ['Client_Name' => 'Acme Corp', 'Amount' => '1250.00'], ], );
The file must be a non-macro Word document. Decoded size is limited to 10 MiB.
Map source fields to placeholders
Use this when your data keys do not match the placeholder names in the template:
$batch = $client->documents()->generate( templateUuid: 'e81d77a2-f674-4b53-a8ee-bf350284e311', data: [ ['client' => 'Acme Corp', 'total' => '1250.00'], ], variableMapping: [ 'client' => 'Client_Name', 'total' => 'Amount_Due', ], );
Indexed rows
If each row is a list of values instead of a map, send the column names separately:
$batch = $client->documents()->generate( templateUuid: 'e81d77a2-f674-4b53-a8ee-bf350284e311', data: [ ['Acme Corp', 'INV-2026-001', '1250.00'], ['Globex Industries', 'INV-2026-002', '3400.00'], ], variables: ['Client_Name', 'Invoice_Number', 'Amount_Due'], );
Request builder
GenerateRequest is immutable. Each with*() method returns a new instance:
use Trydoku\DTO\GenerateRequest; $request = GenerateRequest::create() ->withTemplateUuid('e81d77a2-f674-4b53-a8ee-bf350284e311') ->withData([ ['Client_Name' => 'Acme Corp'], ]) ->withFormat('zip'); $batch = $client->documents()->generateFromRequest($request);
Setting withTemplateUuid() clears a previously set Base64 template, and the other way around.
Idempotency
Pass a stable key so a retried POST /v1/generate does not create a second batch. Successful 2xx responses are replayed for 24 hours for the same user, key, and body.
$batch = $client->documents()->generate( templateUuid: 'e81d77a2-f674-4b53-a8ee-bf350284e311', data: [ ['Client_Name' => 'Acme Corp'], ], idempotencyKey: 'invoice-run-2026-09-17', );
Or on the builder:
$request = GenerateRequest::create() ->withTemplateUuid('e81d77a2-f674-4b53-a8ee-bf350284e311') ->withData([['Client_Name' => 'Acme Corp']]) ->withIdempotencyKey('invoice-run-2026-09-17'); $batch = $client->documents()->generateFromRequest($request);
The key must be 1–255 characters of A–Z, a–z, 0–9, ., _, or -. Empty or whitespace-only values are omitted.
If a request with that key is still running, the API returns HTTP 409 (ConflictException). Retry the same key — a new key after 409 can duplicate work. If the key is reused with a different body, the API returns HTTP 422 (IdempotencyKeyConflictException).
Check batch status
$batch = $client->batches()->get('a91b22e1-a714-4113-a83d-bc350284e300'); echo $batch->id; // "a91b22e1-..." echo $batch->status; // "processing" | "completed" | "failed" echo $batch->totalItems; // 2 echo $batch->processedItems; // 2 echo $batch->failedItems; // 0 echo $batch->links->zip; // ZIP URL once the batch has completed, otherwise null $batch->isCompleted(); // true when status === "completed" $batch->isProcessing(); // true when status === "processing" $batch->isSetupPending(); // true for HTTP 202 GENERATION_SETUP_PENDING $batch->hasFailed(); // true when failedItems > 0 foreach ($batch->items as $item) { echo "Row {$item->rowIndex}: {$item->status}"; if ($item->downloadUrl) { echo " → {$item->downloadUrl}"; } if ($item->error) { echo " [{$item->error}]"; } echo "\n"; }
isCompleted() means the batch finished, not that every row succeeded. Always check hasFailed() before you treat the ZIP as complete. If isSetupPending() is true, keep polling — setup has not finished yet.
Wait for completion
$batch = $client->batches()->waitForCompletion( batchId: $batch->id, maxAttempts: 30, // default 30 intervalMs: 2000, // default 2000 ms ); if ($batch->isCompleted() && !$batch->hasFailed()) { $zip = $client->batches()->downloadZip($batch->id); file_put_contents('result.zip', $zip); }
Polling stops when the batch completes, when status or setupStatus is "failed", or when $maxAttempts is used up. A 202 setup-pending batch keeps polling. A failed row does not stop polling while other rows are still running.
$maxAttempts must be at least 1. $intervalMs must be zero or a positive number that fits in microseconds.
If the batch is still processing after the last attempt, waitForCompletion() throws ApiException.
Download a ZIP
$zipContent = $client->batches()->downloadZip('a91b22e1-a714-4113-a83d-bc350284e300'); file_put_contents('result.zip', $zipContent);
Downloading before the batch has completed throws BatchNotReadyException.
Error handling
API errors throw a subclass of TrydokuException. Catch the specific type when you need its extra fields; catch TrydokuException for everything else.
Transport errors (Psr\Http\Client\ClientExceptionInterface), bad local arguments (InvalidArgumentException), and JSON encoding failures (JsonException) are not wrapped. They keep their original types.
use Trydoku\Exception\AuthenticationException; use Trydoku\Exception\InsufficientCreditsException; use Trydoku\Exception\TrydokuException; use Trydoku\Exception\ValidationException; try { $batch = $client->documents()->generate( templateUuid: 'e81d77a2-f674-4b53-a8ee-bf350284e311', data: [['Name' => 'Test']], ); } catch (AuthenticationException $e) { // 401 — missing or invalid token echo "Auth error: {$e->getMessage()}\n"; } catch (InsufficientCreditsException $e) { // 402 — not enough credits for this batch echo "Credits: {$e->creditsAvailable} available, {$e->creditsRequired} required\n"; } catch (ValidationException $e) { // 422 — payload or template schema mismatch foreach ($e->getErrors() as $field => $messages) { echo "{$field}: " . implode(', ', $messages) . "\n"; } } catch (TrydokuException $e) { echo "Error [{$e->httpStatusCode}]: {$e->getMessage()}\n"; echo "Response: {$e->responseBody}\n"; }
| Exception | HTTP | When |
|---|---|---|
AuthenticationException |
401 | Token missing or invalid |
InsufficientCreditsException |
402 | Not enough credits for the request |
AuthorizationException |
403 | Token lacks permission |
BatchNotReadyException |
400 | ZIP requested before the batch completed |
ConflictException |
409 | Idempotency-Key is already in progress — retry the same key |
PayloadTooLargeException |
413 | Request body larger than 20 MiB |
UnsupportedMediaTypeException |
415 | Content-Encoding is set and is not identity |
ValidationException |
422 | Invalid payload or template schema mismatch |
IdempotencyKeyConflictException |
422 | Idempotency-Key reused with a different body |
GenerationSetupFailedException |
503 | Generation could not start; credits were refunded |
ApiException |
* | Any other API error, invalid JSON, incomplete payload, or polling timeout |
API reference
- API overview — authentication, payloads, and examples
- Interactive reference — OpenAPI explorer
| Method | Endpoint | SDK method |
|---|---|---|
POST |
/v1/generate |
$client->documents()->generate(...) |
GET |
/v1/batches/{id} |
$client->batches()->get($id) |
GET |
/v1/batches/{id}/zip |
$client->batches()->downloadZip($id) |
Development
Pull requests run unit tests and lint on PHP 8.2, 8.3, and 8.4 via GitHub Actions.
composer install # Unit tests composer test # Integration tests against the live API TRYDOKU_API_TOKEN=xxx TRYDOKU_TEMPLATE_UUID=yyy ./vendor/bin/phpunit --testsuite integration # Mutation tests ./vendor/bin/infection --threads=4 # Code style composer lint # check composer format # rewrite
Contributing
Bug reports and pull requests are welcome. Please:
- Open an issue first for larger changes.
- Keep the public API backwards-compatible unless the change is a major version.
- Add or update unit tests for the behaviour you change.
- Run
composer testandcomposer lintbefore you open a pull request.
Security
Please report vulnerabilities privately. See SECURITY.md.
License
Released under the MIT License.
Support
- Documentation: trydoku.com/docs
- API reference: trydoku.com/docs/api/reference
- Email: support@trydoku.com