Generate PDF documents with Python
Posted by admin
Creating Documents is universal truth across organizations. Every organization either big or small.
EDocGen is a powerful, template-driven automation platform that streamlines document generation. Its versatility allows teams to instantly generate files in multiple formats—including PDF, DOCX, PPTX, and XLSX—directly from their data. With built-in support for bulk document generation from databases, and automated email delivery, the system easily scales to handle high-volume enterprise workflows.
For web development, PHP remains a dominant server-side scripting language due to its simplicity, speed, and vast ecosystem. Integrating EDocGen’s API into your existing PHP application is highly straightforward, allowing you to add robust document automation to your business processes with minimal overhead. This article walks you through the entire integration process, built on PHP 8.4/8.5 using a modern, type-safe HTTP stack.
mkdir php-pdf-generation && cd php-pdf-generation
composer init --no-interaction --require=php:^8.4
composer require guzzlehttp/guzzle vlucas/phpdotenv
composer.json:
{
"name": "edocgen/php-pdf-generation-example",
"description": "Template-based PDF document generation example using the EDocGen REST API",
"type": "project",
"require": {
"php": "^8.4",
"guzzlehttp/guzzle": "^7.9",
"vlucas/phpdotenv": "^5.6"
},
"autoload": {
"psr-4": {
"EDocGen\\": "src/"
}
},
"config": {
"sort-packages": true
}
}
Copy .env.example to .env and fill in your real credentials — this file is never committed to source control:
EDOCGEN_BASE_URL=https://app.edocgen.com/
EDOCGEN_USERNAME=
EDOCGEN_PASSWORD=
EDOCGEN_DEFAULT_TEMPLATE_ID=
composer.json
.env.example
src/
EDocGenClient.php
Enums/
OutputFormat.php
Exceptions/
EDocGenException.php
LoginException.php
DocumentGenerationException.php
Support/
DatabaseSource.php
examples/
bootstrap.php
generate-from-json.php
generate-from-database.php
email-output.php
EDocGenClient: The single class that talks to the EDocGen API — login, template lookup, document generation from JSON or a database, polling for output, downloading, and emailing.
OutputFormat: A backed enum (Pdf, Docx, Pptx) that replaces the loose "pdf" string literals scattered through the original functions.
DatabaseSource: A small readonly value object holding the database vendor, connection URL, query, and row limit for database-driven generation, replacing the long list of loose $db* variables in the original downloadBulkUsingDB() function.
Exceptions: LoginException and DocumentGenerationException (both extending a base EDocGenException) so calling code can catch specific failure modes instead of checking if ($response === false) after every call.
To facilitate all API interactions, the API uses a JWT-based authentication token. To acquire it, register and submit your login credentials to the /login endpoint. You’ll then receive an access token that must be included in every subsequent API call.
That part of the flow hasn’t changed. What’s different is where the credentials come from and what happens if login fails. Here’s the relevant piece of EDocGenClient:
final class EDocGenClient
{
private readonly Client $http;
private ?string $token = null;
public function __construct(
string $baseUrl,
private readonly string $username,
private readonly string $password,
) {
$this->http = new Client([
'base_uri' => rtrim($baseUrl, '/') . '/',
'timeout' => 30,
]);
}
/**
* Authenticates against the /login endpoint and caches the access token
* for subsequent calls. Called automatically on first use — you rarely
* need to call this directly.
*/
public function login(): string
{
$response = $this->request('POST', 'login', [
RequestOptions::JSON => [
'username' => $this->username,
'password' => $this->password,
],
], LoginException::class);
$data = $this->decodeJson((string) $response->getBody());
if (empty($data['token'])) {
throw new LoginException('Login response did not include an access token.');
}
return $this->token = $data['token'];
}
private function authHeaders(): array
{
return ['x-access-token' => $this->token ?? $this->login()];
}
}
username and password are constructor-promoted readonly properties — PHP 8.0+ shorthand that declares, types, and assigns them in one line instead of the four separate $this->foo = $foo assignments the original constructor would have needed. authHeaders() logs in automatically the first time it’s called and reuses the cached token afterward, so every other method just calls $this->authHeaders() without worrying about whether login has already happened.
Every request in the client goes through one shared request() helper that converts any Guzzle failure into a typed exception instead of a raw GuzzleException (or worse, a silently swallowed false):
private function request(string $method, string $uri, array $options, string $exceptionClass = EDocGenException::class): ResponseInterface
{
try {
return $this->http->request($method, $uri, $options);
} catch (GuzzleException $e) {
throw new $exceptionClass(
"EDocGen API request failed ({$method} {$uri}): {$e->getMessage()}",
previous: $e,
);
}
}
A template acts as a blueprint for the generated document, and the system supports PDF, PPTX, XLSX and DOCX as input template formats. In a template, tags like {Enter_Name} and {Enter_Email} are dynamic and get replaced with real data at generation time.
Once a template is ready, it can be uploaded from the EDocGen UI, or via the API:
| POST | /api/v1/document | Upload template to the system |
|---|---|---|
| documentFile * | file (formData) | File to upload |
| x-access-token | string (header) | Authorization header obtained by calling login |
After uploading a template, you’ll want its ID to use in later calls:
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/document | Returns template details |
/**
* Looks up template IDs by the file name they were uploaded with.
*
* @return string[] Matching template (document) IDs.
*/
public function getTemplateIdsByFileName(string $fileName): array
{
$response = $this->request('GET', 'api/v1/document', [
RequestOptions::QUERY => [
'search_column' => 'filename',
'search_value' => $fileName,
],
RequestOptions::HEADERS => $this->authHeaders(),
]);
$data = $this->decodeJson((string) $response->getBody());
return array_map(
static fn(array $document): string => $document['_id'],
$data['documents'] ?? [],
);
}
This replaces the original getTemplateIdviaFileName() function, which opened a raw cURL handle by hand and echo’d each matching ID. Here, the method returns a plain array of IDs — what you do with them (log them, render them, pick one) is up to the calling code, which is a better fit for a reusable library than baking echo statements into the client itself.
The system supports generating multiple documents with a single API call — useful for producing customized documents in bulk. A template must be uploaded first, and the data source can be JSON, XLSX, or XML, sent to /api/v1/document/generate/bulk.
There are two modes:
.zip file automatically.
/**
* Submits a JSON data file for bulk document generation.
*
* @return string The generated output file name, used to poll for completion.
*/
public function generateFromJson(
string $jsonFilePath,
string $documentId,
OutputFormat $format = OutputFormat::Pdf,
): string {
if (!is_readable($jsonFilePath)) {
throw new DocumentGenerationException("Cannot read input file: {$jsonFilePath}");
}
$outputFileName = $this->generateOutputFileName();
$this->request('POST', 'api/v1/document/generate/bulk', [
RequestOptions::HEADERS => $this->authHeaders(),
RequestOptions::MULTIPART => [
[
'name' => 'inputFile',
'contents' => fopen($jsonFilePath, 'r'),
'filename' => basename($jsonFilePath),
],
['name' => 'documentId', 'contents' => $documentId],
['name' => 'format', 'contents' => $format->value],
['name' => 'outputFileName', 'contents' => $outputFileName],
],
], DocumentGenerationException::class);
return $outputFileName;
}
This is the same multipart upload the original downloadBulkUsingJson() function performed, but built with Guzzle’s RequestOptions::MULTIPART instead of CURLOPT_POSTFIELDS and a CURLFILE instance, and it no longer mixes the HTTP call together with the polling and download logic — those are now separate methods you call explicitly (see below), so a caller that only wants to kick off generation and check back later doesn’t have to pull in the retry loop too.
Generating documents directly from a database query is also unchanged at the API level. What’s changed is that the long list of loose $db* variables from the original downloadBulkUsingDB() function is now a single typed DatabaseSource value object:
final readonly class DatabaseSource
{
public function __construct(
public string $vendor,
public string $url,
public string $password,
public string $query,
public int $limit,
public string $keyToFileName = 'Prefix',
) {
}
}
/**
* Submits a database query as the data source for bulk document generation.
*
* @return string The generated output file name, used to poll for completion.
*/
public function generateFromDatabase(
DatabaseSource $source,
string $documentId,
OutputFormat $format = OutputFormat::Pdf,
): string {
$outputFileName = $this->generateOutputFileName();
$this->request('POST', 'api/v1/document/generate/bulk', [
RequestOptions::HEADERS => $this->authHeaders(),
RequestOptions::FORM_PARAMS => [
'dbVendor' => $source->vendor,
'dbUrl' => $source->url,
'dbPassword' => $source->password,
'dbQuery' => $source->query,
'dbLimit' => (string) $source->limit,
'documentId' => $documentId,
'format' => $format->value,
'outputFileName' => $outputFileName,
'keyToFileName' => $source->keyToFileName,
],
], DocumentGenerationException::class);
return $outputFileName;
}
Passing a DatabaseSource object instead of six separate scalar arguments means the constructor is self-documenting and PHP will catch a missing or mistyped field (say, passing a string where limit expects an int) before the request ever goes out.
Both generation paths return an output file name that needs to be polled until EDocGen finishes processing it. The original example did this with a while loop that retried immediately with no delay between attempts. This version polls on a fixed interval and gives up with a clear exception if the output never shows up:
/**
* Polls the output endpoint until the generated file is ready (or retries run out).
*/
public function waitForOutput(
string $outputFileName,
OutputFormat $format = OutputFormat::Pdf,
int $maxRetries = 50,
int $delaySeconds = 2,
): string {
for ($attempt = 1; $attempt <= $maxRetries; $attempt++) {
$outputId = $this->findOutputId($outputFileName, $format);
if ($outputId !== null) {
return $outputId;
}
sleep($delaySeconds);
}
throw new DocumentGenerationException(
"Output '{$outputFileName}' was not ready after {$maxRetries} attempts.",
);
}
/**
* Downloads a generated output to a local file path.
*/
public function downloadOutput(string $outputId, string $destinationPath): void
{
$this->request('GET', "api/v1/output/download/{$outputId}", [
RequestOptions::HEADERS => $this->authHeaders(),
RequestOptions::SINK => $destinationPath,
], DocumentGenerationException::class);
}
RequestOptions::SINK tells Guzzle to stream the response straight to a file on disk instead of loading it into memory — worth knowing about if you’re downloading large .zip archives of bulk-generated documents.
Sending a generated document by email — useful for delivering invoices, reports, or contracts straight to a recipient — is a single call:
/**
* Emails a generated output to the given address.
*/
public function emailOutput(string $outputId, string $emailAddress): void
{
$this->request('POST', 'api/v1/output/email', [
RequestOptions::HEADERS => $this->authHeaders(),
RequestOptions::JSON => [
'outId' => $outputId,
'emailId' => $emailAddress,
],
]);
}
With the client written, a full generate-and-download run is a few lines:
<?php
declare(strict_types=1);
require __DIR__ . '/bootstrap.php';
use EDocGen\Enums\OutputFormat;
use EDocGen\Exceptions\EDocGenException;
$documentId = $_ENV['EDOCGEN_DEFAULT_TEMPLATE_ID']
?? throw new RuntimeException('EDOCGEN_DEFAULT_TEMPLATE_ID is not set');
$jsonFilePath = __DIR__ . '/JSON_Data.json';
try {
$client = edocgenClient();
$outputFileName = $client->generateFromJson($jsonFilePath, $documentId, OutputFormat::Pdf);
echo "Submitted for generation as '{$outputFileName}', waiting for output...\n";
$outputId = $client->waitForOutput($outputFileName, OutputFormat::Pdf);
$destination = __DIR__ . "/output/{$outputFileName}.zip";
$client->downloadOutput($outputId, $destination);
echo "Downloaded to {$destination}\n";
} catch (EDocGenException $e) {
fwrite(STDERR, "Document generation failed: {$e->getMessage()}\n");
exit(1);
}
bootstrap.php loads .env with vlucas/phpdotenv and builds the client once, so every example script shares the same setup:
<?php
declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php';
use Dotenv\Dotenv;
use EDocGen\EDocGenClient;
Dotenv::createImmutable(__DIR__ . '/..')->safeLoad();
function edocgenClient(): EDocGenClient
{
return new EDocGenClient(
baseUrl: $_ENV['EDOCGEN_BASE_URL'] ?? 'https://app.edocgen.com/',
username: $_ENV['EDOCGEN_USERNAME'] ?? throw new RuntimeException('EDOCGEN_USERNAME is not set'),
password: $_ENV['EDOCGEN_PASSWORD'] ?? throw new RuntimeException('EDOCGEN_PASSWORD is not set'),
);
}
Note the ?? throw expressions — a PHP 8.0 feature that lets you fail fast on missing configuration in a single line, instead of an if (!isset(...)) block ahead of every use.
Run it with:
php examples/generate-from-json.php
EDocGen remains an excellent solution for businesses looking to automate document generation — eliminating manual input, reducing human error, and freeing teams to focus on higher-value work. Generating documents in multiple formats, and emailing them directly, can be seamlessly handled with just a few API calls.
Posted by admin
Creating Documents is universal truth across organizations. Every organization either big or small.
Posted by admin
Integrate with the Azure SQL Server to generate PDF documents from existing templates.
Posted by admin
Generate PDF documents from existing templates and send them by email from .Net application.