koba / digitaal-ondertekenen-php
PHP wrapper for the Vlaamse overheid Digitaal Ondertekenen API.
Package info
github.com/kobavzw/digitaal-ondertekenen-php
pkg:composer/koba/digitaal-ondertekenen-php
Requires
- php: ^8.1
- ext-dom: *
- json-mapper/json-mapper: ^2.25
- league/oauth2-client: ^2.9
- php-http/discovery: ^1.20
- psr/http-client: ^1.0
- psr/http-client-implementation: ^1.0
- psr/http-factory: ^1.1
- psr/http-factory-implementation: ^1.0
- psr/http-message: ^2.0
Requires (Dev)
- phpstan/phpstan: ^2.2
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
PHP client for the Vlaamse overheid Digitaal Ondertekenen API. It handles the two-stage OAuth authentication flow and provides typed calls for creating a document package, uploading a document, configuring its signing workflow, and opening the integrated signing viewer.
This is an unofficial client. You need credentials and a configured profile for the Digitaal Ondertekenen service to use it.
Requirements
- PHP 8.1 or newer
- An ACM OAuth client ID and secret
- A Digitaal Ondertekenen client ID and secret
- A PSR-18 HTTP client and PSR-17 HTTP factories
Installation
Install the library and an HTTP implementation with Composer. Guzzle provides both the PSR-18 client and the PSR-17 factories required by this package:
composer require koba/digitaal-ondertekenen-php guzzlehttp/guzzle
If your application already supplies compatible PSR-17 and PSR-18 implementations, only install the library.
Create a client
Keep all four credentials outside your source code, for example in environment variables:
<?php require __DIR__ . '/vendor/autoload.php'; use Koba\DigitaalOndertekenen\ClientFactory; use Koba\DigitaalOndertekenen\Environment; $client = (new ClientFactory())->create( vlaanderenClientId: $_ENV['VLAANDEREN_CLIENT_ID'], vlaanderenClientSecret: $_ENV['VLAANDEREN_CLIENT_SECRET'], digitaalOndertekenenClientId: $_ENV['DIGITAAL_ONDERTEKENEN_CLIENT_ID'], digitaalOndertekenenClientSecret: $_ENV['DIGITAAL_ONDERTEKENEN_CLIENT_SECRET'], environment: Environment::TEST, );
Use Environment::TEST for the test/integration environment and
Environment::PRODUCTION for production. Production is the default when the
environment argument is omitted.
HTTP requests are made when send() is called, not when a call object is
created.
Complete signing flow
The following example creates a package for one external signer, uploads a PDF, places a signature field, starts the workflow, and generates a URL for the integrated signing viewer:
<?php use Koba\DigitaalOndertekenen\Api\CollaboratorRole; use Koba\DigitaalOndertekenen\Api\IntegrationResponseType; use Koba\DigitaalOndertekenen\Api\SignatureLevelOfAssurance; use Koba\DigitaalOndertekenen\Api\WorkflowMode; $package = $client->createPackage( workflowMode: WorkflowMode::ONLY_OTHERS, packageName: 'Agreement 2026-001', )->send(); $pdf = file_get_contents(__DIR__ . '/agreement.pdf'); if ($pdf === false) { throw new RuntimeException('Could not read agreement.pdf'); } $document = $client->uploadDocument( packageId: $package->package_id, fileName: 'agreement.pdf', contents: $pdf, source: 'My application', )->send(); $client->addWorkflowRecipient( packageId: $package->package_id, email: 'signer@example.com', name: 'Example Signer', role: CollaboratorRole::SIGNER, signingOrder: 1, )->send(); $client->addSignatureField( packageId: $package->package_id, documentId: $document->document_id, recipientOrder: 1, pageNumber: 1, dimensions: [ 'x' => 100.0, 'y' => 600.0, 'width' => 180.0, 'height' => 60.0, ], levelOfAssurance: SignatureLevelOfAssurance::ELECTRONIC_SIGNATURE, fieldName: 'customer_signature', )->send(); $client->startWorkflow($package->package_id)->send(); $signingUrl = $client->generateIntegrationUrl( packageId: $package->package_id, documentId: $document->document_id, language: 'nl-NL', userEmail: 'signer@example.com', callbackUrl: 'https://example.com/signing/complete', responseType: IntegrationResponseType::PLAIN, collapsePanels: true, lockPanels: false, redirectCallbackUrl: true, )->send(); header('Location: ' . $signingUrl);
recipientOrder identifies the recipient's one-based position in the package
workflow. It is distinct from signingOrder, which controls the processing
stage. Signature-field coordinates and dimensions are pixels; x is measured
from the left and y from the top of the one-based page.
Download a signed document
After the workflow has completed, download the signed document as raw bytes:
$signedPdf = $client->downloadDocument( packageId: $packageId, documentId: $documentId, )->send(); if (file_put_contents(__DIR__ . '/signed-agreement.pdf', $signedPdf) === false) { throw new RuntimeException('Could not save the signed document'); }
The endpoint returns the document's current bytes, so wait for workflow
completion when the signed version is required. The optional password, otp,
and folderId arguments map to SigningHub's x-password, x-otp, and
x-folder-id access headers.
Available operations
Every client method returns a call object. Invoke send() to execute it.
| Method | Result | Purpose |
|---|---|---|
createPackage() |
AddPackageResponse |
Create a draft package. |
uploadDocument() |
UploadDocumentResponse |
Upload raw document bytes to a package. |
downloadDocument() |
string |
Download a document's current raw bytes. |
addWorkflowRecipient() |
WorkflowRecipientResponse |
Add a recipient to the draft workflow. |
addSignatureField() |
AddSignatureFieldResponse |
Add a visible signature field to a document. |
startWorkflow() |
StartWorkflowResponse |
Share the package and start the workflow. |
generateIntegrationUrl() |
string |
Generate a recipient-specific viewer URL. |
The response objects expose the API fields as public readonly properties. The
most commonly needed properties are package_id, document_id, field_name,
and documents.
Workflow options
WorkflowMode:ME_AND_OTHERS,ONLY_OTHERS, orONLY_MECollaboratorRole:SIGNER,REVIEWER,EDITOR,CARBON_COPY, orINPERSON_HOSTSignatureLevelOfAssurance:ELECTRONIC_SIGNATURE,ADVANCED_ELECTRONIC_SIGNATURE,HIGH_TRUST_ADVANCED, orQUALIFIED_ELECTRONIC_SIGNATUREIntegrationResponseType:ENCODEDorPLAIN
The available signature levels and collaborator roles depend on your service plan and Digitaal Ondertekenen configuration.
Error handling
Non-successful API responses throw ApiException. It exposes both the HTTP
status and the original response body:
use Koba\DigitaalOndertekenen\Exception\ApiException; use Koba\DigitaalOndertekenen\Api\WorkflowMode; use League\OAuth2\Client\Provider\Exception\IdentityProviderException; use Psr\Http\Client\ClientExceptionInterface; try { $package = $client->createPackage(WorkflowMode::ONLY_OTHERS)->send(); } catch (ApiException $exception) { error_log(sprintf( 'API error %d: %s', $exception->getStatusCode(), $exception->getResponseBody(), )); } catch (IdentityProviderException $exception) { error_log('Authentication failed: ' . $exception->getMessage()); } catch (ClientExceptionInterface $exception) { error_log('HTTP transport failed: ' . $exception->getMessage()); }
Argument validation errors, such as an invalid email address, page number, or
field dimensions, throw InvalidArgumentException before the request is sent.
Webhooks
SigningHub can publish document-processing reports to an integration webhook
URL when configured document actions occur. These webhook requests use POST
and contain XML. The webhook parser accepts either a PSR-7 server request or
the raw request body:
use Koba\DigitaalOndertekenen\Exception\InvalidWebhookException; use Koba\DigitaalOndertekenen\Webhook\WebhookActionType; use Koba\DigitaalOndertekenen\Webhook\WebhookParser; try { $event = WebhookParser::parseXml( file_get_contents('php://input') ?: '', ); // Store this in a table with a unique constraint to make retries harmless. $idempotencyKey = $event->getFingerprint(); $packageId = $event->getPackageId(); $action = $event->getAction(); switch ($action->type) { case WebhookActionType::SIGNED: // A participant signed. This does not necessarily mean that the // package is complete when more workflow steps remain. break; case WebhookActionType::COMPLETED: // The package reached its terminal completed state. break; case WebhookActionType::EVIDENCE_REPORT_GENERATED: // SigningHub finished generating the evidence report. break; } $documents = $event->getDocuments(); $workflowUsers = $event->getWorkflowUsers(); // Queue the work and acknowledge the request promptly. } catch (InvalidWebhookException $exception) { http_response_code(400); exit; } http_response_code(204);
When a framework provides a ServerRequestInterface, use
WebhookParser::parseRequest() to validate the HTTP method as well. The typed
accessors expose the package identity and status, the current action, the next
signer, workflow state, documents, and workflow users. getRawXml() remains
available for fields that the library does not model yet.
A completed one-signer flow can produce three requests in quick succession:
SIGNED, COMPLETED, and EVIDENCE_REPORT_GENERATED. Branch on
$event->getAction()->type; the status values are snapshots and may lag during
the earlier event. Unknown future action types produce a null enum value and
remain available through $event->getAction()->raw_type. Deliveries can also
be retried or arrive out of order, so make handlers idempotent and do not use
arrival order as workflow state.
WebhookActionType contains the documented SHARED, SIGNED, REVIEWED,
DECLINED, EDITED, CARBON_COPIED, RECALLED, REMINDED, COMPLETED,
EVIDENCE_REPORT_GENERATED, and DOCUMENT_DELETED actions.
WebhookProcessStatus similarly models a workflow user's processing status.
Unknown values remain accessible through raw_type and raw_process_status,
respectively, so new SigningHub values do not cause parsing to fail.
Parsing validates safe XML, the DocumentProcessingInformation root, and all
modeled fields before returning an event. Optional document and workflow-user
collections are represented by empty arrays when absent. Parsing does not
establish who sent the request. Observed SigningHub deliveries do not include a
credential or cryptographic signature header, so production applications must
protect the endpoint at the network or gateway layer and confirm consequential
state changes through the API. getFingerprint() provides a SHA-256 fallback
idempotency key for the exact received body because the payload does not contain
an event ID.
Workflow completion reports and attached completed documents are separate
outputs and are not parsed by WebhookParser.
Token caching
By default, each client keeps the Digitaal Ondertekenen access token in memory
for the lifetime of that client instance. Long-running or multi-process
applications can pass an implementation of TokenCacheInterface as the
tokenCache argument to ClientFactory::create(). A cache implementation must
store and return CachedToken values and implement get(), save(), and
delete().
Custom API HTTP implementations
The factory normally discovers installed PSR implementations automatically. The PSR client and factories used for Digitaal Ondertekenen API calls can also be supplied explicitly, which is useful for framework integration or testing:
$factory = new ClientFactory( httpClient: $psr18Client, requestFactory: $psr17RequestFactory, streamFactory: $psr17StreamFactory, );
License
This library is released under the MIT License.