Search by

koba / digitaal-ondertekenen-php

toolbox-kobavzw

PHP wrapper for the Vlaamse overheid Digitaal Ondertekenen API.

Package info

github.com/kobavzw/digitaal-ondertekenen-php

pkg:composer/koba/digitaal-ondertekenen-php

Statistics

Installs: 17

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

1.1.0 2026-09-15 08:42 UTC

This package is auto-updated.

Last update: 2026-10-02 15:58:43 UTC


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, or ONLY_ME
  • CollaboratorRole: SIGNER, REVIEWER, EDITOR, CARBON_COPY, or INPERSON_HOST
  • SignatureLevelOfAssurance: ELECTRONIC_SIGNATURE, ADVANCED_ELECTRONIC_SIGNATURE, HIGH_TRUST_ADVANCED, or QUALIFIED_ELECTRONIC_SIGNATURE
  • IntegrationResponseType: ENCODED or PLAIN

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.