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
- phpstan/phpstan: ^2.2
- phpunit/phpunit: ^11.0
- squizlabs/php_codesniffer: ^4.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
The SDK uses runtime discovery to find installed PSR-18 and PSR-17 implementations. The php-http/discovery Composer plugin is optional; it can help Composer auto-install and pin implementations. You can also install implementations manually or provide all three dependencies directly:
require 'vendor/autoload.php'; $httpClient = new GuzzleHttp\Client(); $factory = new GuzzleHttp\Psr7\HttpFactory(); $client = new Trydoku\Client( 'fake-token', httpClient: $httpClient, requestFactory: $factory, streamFactory: $factory, );
Constructing the client does not make an HTTP request. Explicit dependency injection also works when runtime discovery strategies are disabled.
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 checks the source, idempotency key, and row count before sending; DOCX validity and the remaining payload schema are checked by the API.
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.
Values passed to withData(), withVariables(), and withVariableMapping() are snapshotted when set. Request data may contain nulls, scalars, and nested arrays (up to 128 levels); objects and resources are rejected with InvalidArgumentException.
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 this specific 409 can duplicate work. A 409 BATCH_FILES_MISSING response is a BatchFilesMissingException and does not indicate an in-progress idempotency reservation. Unknown 409 responses are generic ApiExceptions. 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 |
BatchFilesMissingException |
409 | Batch files are unavailable |
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