currence / emandates
Supporting libraries for eMandates protocol
Package info
github.com/Betaalvereniging-Nederland/emandates-libraries-php
pkg:composer/currence/emandates
Requires
- php: >=8.1
- ext-curl: *
- ext-dom: *
- ext-openssl: *
- ext-simplexml: *
- robrichards/xmlseclibs: ^3.1
Requires (Dev)
- phpunit/phpunit: ^10.0||^11.0
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-domext-simplexmlext-curlext-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