currence/emandates

Supporting libraries for eMandates protocol

Maintainers

Package info

github.com/Betaalvereniging-Nederland/emandates-libraries-php

pkg:composer/currence/emandates

Statistics

Installs: 322

Dependents: 0

Suggesters: 0

Stars: 5

Open Issues: 3

2.0.0 2026-04-03 13:04 UTC

This package is auto-updated.

Last update: 2026-04-03 13:08:55 UTC


README

PHP library for Dutch electronic SEPA direct debit mandates (eMandates) using the iDx protocol. It provides a complete implementation for issuing, amending, cancelling, and checking the status of eMandates through the Currence eMandates scheme.

Requirements

  • PHP >= 8.1 (compatible through 8.4)
  • PHP extensions:
    • ext-dom
    • ext-simplexml
    • ext-curl
    • ext-openssl

Installation

composer require currence/emandates

Then include the Composer autoloader in your application:

require __DIR__ . '/vendor/autoload.php';

Configuration

The library needs credentials, certificate paths, and acquirer URLs before it can communicate with the eMandates platform. There are three ways to provide configuration.

Option 1: Global config array with Configuration::getDefault()

Create a file (e.g. eMandatesConfig.php) that populates a global array:

global $emandates_config_params;

$emandates_config_params = [
    'passphrase'                   => 'your-private-key-passphrase',
    'keyFile'                      => '/path/to/merchant.key',
    'crtFile'                      => '/path/to/merchant.crt',
    'crtFileAquirer'               => '/path/to/acquirer.crt',
    'crtFileAquirerAlternative'    => '/path/to/acquirer-alt.crt',
    'contractID'                   => '0000000000',
    'contractSubID'                => '0',
    'merchantReturnURL'            => 'https://example.com/return',
    'AcquirerUrl_DirectoryReq'     => 'https://acquirer.example.com/directory',
    'AcquirerUrl_TransactionReq'   => 'https://acquirer.example.com/transaction',
    'AcquirerUrl_StatusReq'        => 'https://acquirer.example.com/status',
    'enableXMLLogs'                => true,
    'logPath'                      => '/path/to/logs',
    'folderNamePattern'            => 'Y-m-d',
    'fileNamePrefix'               => 'emandates',
    'enableInternalLogs'           => true,
    'fileName'                     => 'emandates.log',
];

Then obtain a Configuration object:

use EMandates\Merchant\Configuration\Configuration;

require_once 'eMandatesConfig.php';

$config = Configuration::getDefault();

Option 2: Constructor

Build a Configuration directly:

use EMandates\Merchant\Configuration\Configuration;

$config = new Configuration(
    passphrase: 'your-private-key-passphrase',
    keyFile: '/path/to/merchant.key',
    crtFile: '/path/to/merchant.crt',
    crtFileAquirer: '/path/to/acquirer.crt',
    crtFileAquirerAlternative: '/path/to/acquirer-alt.crt',
    contractID: '0000000000',
    contractSubID: '0',
    merchantReturnURL: 'https://example.com/return',
    AcquirerUrl_DirectoryReq: 'https://acquirer.example.com/directory',
    AcquirerUrl_TransactionReq: 'https://acquirer.example.com/transaction',
    AcquirerUrl_StatusReq: 'https://acquirer.example.com/status',
    enableXMLLogs: true,
    logPath: '/path/to/logs',
    folderNamePattern: 'Y-m-d',
    fileNamePrefix: 'emandates',
    enableInternalLogs: true,
    fileName: 'emandates.log',
);

Option 3: XML config file with Configuration::load()

If you store settings in an XML file:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <appSettings>
    <add key="passphrase" value="your-private-key-passphrase" />
    <add key="keyFile" value="/path/to/merchant.key" />
    <add key="crtFile" value="/path/to/merchant.crt" />
    <!-- ... remaining keys ... -->
  </appSettings>
</configuration>

Load it:

$config = Configuration::load('/path/to/config.xml');

Usage Examples

Creating a Communicator

CoreCommunicator handles SEPA Core mandates. B2BCommunicator extends it for SEPA B2B mandates and adds cancellation support.

use EMandates\Merchant\Communicator\CoreCommunicator;
use EMandates\Merchant\Communicator\B2BCommunicator;
use EMandates\Merchant\Configuration\Configuration;

// Using explicit configuration
$config = Configuration::getDefault();
$coreCommunicator = new CoreCommunicator($config);
$b2bCommunicator  = new B2BCommunicator($config);

// Or rely on the default (reads from global $emandates_config_params)
$coreCommunicator = new CoreCommunicator();
$b2bCommunicator  = new B2BCommunicator();

1. Directory Request -- Get List of Debtor Banks

$directoryResponse = $coreCommunicator->Directory();

if ($directoryResponse->IsError) {
    echo 'Error: ' . $directoryResponse->Error->ErrorMessage;
} else {
    foreach ($directoryResponse->DebtorBanks as $bank) {
        echo $bank->DebtorBankId . ' - ' . $bank->DebtorBankName
             . ' (' . $bank->DebtorBankCountry . ')' . PHP_EOL;
    }
}

2. New Mandate -- Issue a New eMandate

use EMandates\Merchant\Request\NewMandateRequest;

$request = new NewMandateRequest(
    entranceCode:     'unique-entrance-code-123',
    language:         'en',
    messageId:        '',                         // leave empty to auto-generate
    eMandateId:       'MANDATE-2025-001',
    eMandateReason:   'Monthly subscription',
    debtorReference:  'CUST-42',
    debtorBankId:     'INGBNL2A',
    purchaseId:       'ORDER-789',
    sequenceType:     'RCUR',                     // 'RCUR' (recurring) or 'OOFF' (one-off)
    maxAmount:        '',                         // optional, B2B only
    expirationPeriod: new \DateInterval('PT30M'), // optional, e.g. 30 minutes
);

$response = $coreCommunicator->NewMandate($request);

if ($response->IsError) {
    echo 'Error: ' . $response->Error->ErrorMessage;
} else {
    // Save the transaction ID for the status request later
    $transactionId = $response->TransactionId;

    // Redirect the debtor to their bank
    if ($response->IssuerAuthenticationUrl) {
        header('Location: ' . $response->IssuerAuthenticationUrl);
        exit;
    }
}

3. Amendment -- Amend an Existing Mandate

use EMandates\Merchant\Request\AmendmentRequest;

$amendRequest = new AmendmentRequest(
    entranceCode:         'unique-entrance-code-456',
    language:             'en',
    eMandateId:           'MANDATE-2025-001',
    eMandateReason:       'Updated subscription terms',
    debtorReference:      'CUST-42',
    debtorBankId:         'INGBNL2A',
    purchaseId:           'ORDER-789',
    sequenceType:         'RCUR',
    originalIBAN:         'NL91ABNA0417164300',
    originalDebtorBankId: 'ABNANL2A',
    messageId:            '',                         // optional, auto-generated if empty
    expirationPeriod:     new \DateInterval('PT20M'),  // optional
);

$amendResponse = $coreCommunicator->Amend($amendRequest);

if ($amendResponse->IsError) {
    echo 'Error: ' . $amendResponse->Error->ErrorMessage;
} else {
    $transactionId = $amendResponse->TransactionId;
    if ($amendResponse->IssuerAuthenticationUrl) {
        header('Location: ' . $amendResponse->IssuerAuthenticationUrl);
        exit;
    }
}

4. Cancellation -- Cancel a Mandate (B2B only)

Cancellation is only available through B2BCommunicator.

use EMandates\Merchant\Request\CancellationRequest;

$cancelRequest = new CancellationRequest(
    entranceCode:     'unique-entrance-code-789',
    language:         'en',
    eMandateId:       'MANDATE-2025-001',
    eMandateReason:   'Contract terminated',
    debtorReference:  'CUST-42',
    debtorBankId:     'INGBNL2A',
    purchaseId:       'ORDER-789',
    sequenceType:     'RCUR',
    originalIBAN:     'NL91ABNA0417164300',
    messageId:        '',                         // optional
    maxAmount:        '1000.00',                  // optional, B2B only
    expirationPeriod: new \DateInterval('PT20M'),  // optional
);

$cancelResponse = $b2bCommunicator->Cancel($cancelRequest);

if ($cancelResponse->IsError) {
    echo 'Error: ' . $cancelResponse->Error->ErrorMessage;
} else {
    $transactionId = $cancelResponse->TransactionId;
    if ($cancelResponse->IssuerAuthenticationUrl) {
        header('Location: ' . $cancelResponse->IssuerAuthenticationUrl);
        exit;
    }
}

5. Get Status -- Check Transaction Status

After the debtor returns from the bank, check the transaction outcome.

use EMandates\Merchant\Request\StatusRequest;
use EMandates\Merchant\Enum\TransactionStatus;

$statusRequest = new StatusRequest(TransactionId: $transactionId);
$statusResponse = $coreCommunicator->GetStatus($statusRequest);

if ($statusResponse->IsError) {
    echo 'Error: ' . $statusResponse->Error->ErrorMessage;
} else {
    // $statusResponse->Status is a TransactionStatus enum instance
    switch ($statusResponse->Status) {
        case TransactionStatus::Success:
            $report = $statusResponse->AcceptanceReport;
            echo 'Mandate accepted!' . PHP_EOL;
            echo 'Debtor IBAN: ' . $report->DebtorIBAN . PHP_EOL;
            echo 'Debtor name: ' . $report->DebtorAccountName . PHP_EOL;
            echo 'Debtor bank: ' . $report->DebtorBankId . PHP_EOL;
            break;

        case TransactionStatus::Open:
        case TransactionStatus::Pending:
            echo 'Transaction is still in progress.' . PHP_EOL;
            break;

        case TransactionStatus::Cancelled:
            echo 'Transaction was cancelled by the debtor.' . PHP_EOL;
            break;

        case TransactionStatus::Expired:
            echo 'Transaction has expired.' . PHP_EOL;
            break;

        case TransactionStatus::Failure:
            echo 'Transaction failed.' . PHP_EOL;
            break;
    }

    // If you need the raw string value:
    echo 'Status string: ' . $statusResponse->Status->value . PHP_EOL;
}

Custom Logger

Implement LoggerInterface to capture log output (e.g. to a database):

use EMandates\Merchant\Contract\LoggerInterface;

class DBLogger implements LoggerInterface
{
    public function Log(string $message): void
    {
        // Store $message in your database or logging system
    }

    public function LogXmlMessage(\DOMDocument|string $dom, bool $isXML = false, string $fileName = ''): void
    {
        // Store XML request/response messages
        $xml = $dom instanceof \DOMDocument ? $dom->saveXML() : $dom;
        // ... persist $xml
    }
}

Pass the logger when constructing a communicator:

$logger = new DBLogger();
$communicator = new CoreCommunicator(Configuration::getDefault(), $logger);

Error Handling

All communicator methods (Directory, NewMandate, Amend, Cancel, GetStatus) return response objects that include error information rather than throwing exceptions to the caller. This includes acquirer errors, unexpected response formats, missing XML elements, and non-XML error messages.

$response = $coreCommunicator->NewMandate($request);

if ($response->IsError) {
    $error = $response->Error;

    echo 'Error code: '      . $error->ErrorCode      . PHP_EOL;
    echo 'Error message: '   . $error->ErrorMessage    . PHP_EOL;
    echo 'Error details: '   . $error->ErrorDetails    . PHP_EOL;
    echo 'Suggested action: '. $error->SuggestedAction  . PHP_EOL;
    echo 'Consumer message: '. $error->ConsumerMessage   . PHP_EOL;
}

The ErrorResponse object provides these properties:

Property Description
ErrorCode Machine-readable error code from the acquirer
ErrorMessage Human-readable error description
ErrorDetails Additional detail about the error
SuggestedAction Recommended corrective action
ConsumerMessage Message suitable for display to the end user

Note: Transaction response objects (NewMandateResponse, AmendmentResponse, CancellationResponse) also accept AcquirerStatusRes XML. In this case the response will not have an IssuerAuthenticationUrl, so always use null-safe access:

if ($response->IssuerAuthenticationUrl) {
    header('Location: ' . $response->IssuerAuthenticationUrl);
    exit;
}

If a communication-level error occurs (e.g. invalid XML response, schema validation failure), a CommunicatorException may be thrown internally. This exception provides errorCode and errorMessage as readonly properties:

use EMandates\Merchant\Exception\CommunicatorException;

try {
    $response = $coreCommunicator->NewMandate($request);
} catch (CommunicatorException $e) {
    echo $e->errorCode;
    echo $e->errorMessage;
}

Upgrading from the Legacy Version

If you are upgrading from the older PHP 5.5+ version of this library, see UPGRADE.md for a complete list of breaking changes and migration steps.

License

This library is released under the MIT License.

Copyright (c) 2018 Currence-Online