mirrorps / taler-php
PHP SDK for GNU Taler REST API
Installs: 7
Dependents: 0
Suggesters: 0
Security: 0
Stars: 4
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/mirrorps/taler-php
Requires
- php: ^8.1
- league/uri: ^7.5
- psr-discovery/http-client-implementations: ^1.0
- psr-discovery/http-factory-implementations: ^1.0
- psr/http-client: ^1.0
- psr/http-client-implementation: *
- psr/http-factory: ^1.0
- psr/log: ^3.0
- psr/simple-cache: ^3.0
Requires (Dev)
- guzzlehttp/guzzle: ^7.0
- monolog/monolog: ^3.0
- nyholm/psr7: ^1.8
- php-http/httplug: ^2.4
- php-http/mock-client: ^1.6
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^10.5
- symfony/var-dumper: ^6.4
Suggests
- ext-zlib: For optional gzip compression of large request bodies
- php-http/async-client-implementation: For async HTTP requests (e.g., symfony/http-client, php-http/guzzle7-adapter)
- psr/log-implementation: For logging support (e.g., monolog/monolog)
This package is auto-updated.
Last update: 2025-12-03 14:08:24 UTC
README
TalerPHP is a PHP SDK for interacting with GNU Taler payment systems. It provides a simple, secure way to integrate Taler payments into your PHP applications and services.
⚠️ Early Development Notice
TalerPHP is currently in early development. The API and features are subject to change, and the package is not yet ready for production use. We welcome feedback and contributions, but please use with caution and do not rely on this package for critical or production systems at this stage
Features
- Easy API for interacting with Taler services
- PSR-4 autoloading
- Extensible and testable architecture
Prerequisites
Before you begin, ensure you have met the following requirements:
- PHP 8.1 or newer
- Composer
- (Optional) PHPUnit for running tests
Supported Taler protocol versions
- Supported Taler protocol versions: v12-v20
Installation
Install TalerPHP via Composer:
composer require mirrorps/taler-php
Requirements
• A PSR-18 HTTP client implementation (e.g., Guzzle, Symfony HttpClient)
• A PSR-17 HTTP factory implementation (e.g., Nyholm/psr7, guzzlehttp/psr7 version 2 or higher)
• Optional: For async request support, you need a client that implements the `HttpAsyncClient` interface from `php-http/httplug`
Usage
require "vendor/autoload.php";
use Taler\Factory\Factory;
$taler = Factory::create([
'base_url' => 'https://backend.demo.taler.net/instances/sandbox',
'token' => 'Bearer token'
]);
Factory-managed authentication (no pre-existing token)
You can let the SDK obtain and manage the access token for you by providing credentials and the target instance. The SDK will:
- Request an access token using Basic auth
- Store the resulting
Authorizationheader value internally - Refresh the token automatically before it expires when sending requests
use Taler\Factory\Factory; $taler = Factory::create([ 'base_url' => 'https://backend.demo.taler.net/instances/sandbox', 'username' => 'merchant-user', 'password' => 'merchant-pass', 'instance' => 'shop-1', // instance ID // optional (defaults shown): 'scope' => 'readonly', // "readonly"|"write"|"all"|"order-simple"|"order-pos"|"order-mgmt"|"order-full" 'duration_us' => 3600_000_000, // token validity upper bound in microseconds or "forever" 'description' => 'Backoffice session' ]); // Use the client as usual; the SDK injects/refreshes the Authorization token automatically. $orders = $taler->order()->getOrders(['limit' => '-20']);
Retrieve and persist the managed token
After creation, you can read the in-memory token from the client’s config and save it for reuse:
// Extract the token and its expiry from the managed-auth client $token = $taler->getConfig()->getAuthToken(); // e.g., "Bearer abc..." $expiresAt = $taler->getConfig()->getAuthTokenExpiresAtTs(); // int|null (seconds) // Persist to your storage (DB/Secrets Manager/etc.) saveTokenSomewhere($token, $expiresAt);
Later, restore the client with the saved token:
$taler = \Taler\Factory\Factory::create([ 'base_url' => 'https://backend.demo.taler.net/instances/sandbox', 'token' => $tokenFromStorage // already includes the "Bearer " prefix ]);
Note: The token string already includes the required “Bearer ” prefix; store and reuse it as-is. Notes:
- If
tokenis provided, it takes precedence and credentials are ignored. - The password is only used to acquire a token; the SDK stores the resulting access token and its expiry metadata for refresh.
Configuration
base_url: The URL of your Taler backend instance.token: Your authentication token ( ⚠️ do not hardcode; use environment variables or secure storage in your application).username(optional): Merchant instance username for Factory-managed authentication.password(optional): Merchant instance password for Factory-managed authentication.instance(optional): Target instance ID to authenticate against when using credentials.scope(optional): Desired token scope when using credentials. One of"readonly"|"write"|"all"|"order-simple"|"order-pos"|"order-mgmt"|"order-full". Defaults to"readonly".duration_us(optional): Upper bound on token validity as microseconds (int) or"forever". The server may override. Defaults to SDK/server defaults.description(optional): Human-readable description attached to the issued token.wrapResponse: (Optional) Boolean flag to control DTO wrapping of responses. Defaults totrue. When set tofalse, methods return raw array responses from Taler.httpClient: (Optional) A PSR-18 compatible HTTP client instance.logger: (Optional) A PSR-3 compatible logger. If omitted, the SDK performs no logging.debugLoggingEnabled: (Optional) Boolean flag to enable SDK DEBUG logging. Defaults tofalse. Whenfalse, the SDK skips all debug logging work (zero overhead).
Basic Example
$taler = Factory::create([ 'base_url' => 'https://backend.demo.taler.net/instances/sandbox', 'token' => 'Bearer token', 'wrapResponse' => true // Optional, defaults to true ]);
Using Custom HTTP Client
If the SDK's auto-discovery doesn't find a PSR-18 compatible HTTP client, or if you want to use a specific client implementation, you can provide your own. Here's an example using Guzzle:
use Http\Adapter\Guzzle7\Client as GuzzleAdapter; // Create PSR-18 client using Guzzle $httpClient = GuzzleAdapter::createWithConfig([ // Strongly recommended: always set sane timeouts // Overall request timeout (seconds) 'timeout' => 10.0, // Connection establishment timeout (seconds) 'connect_timeout' => 5.0, // Redirect policy: prevent protocol downgrade and limit hops 'allow_redirects' => [ 'max' => 3, // limit redirect chain length 'protocols' => ['https'], // disallow downgrade to http 'referer' => false ], ]); $taler = Factory::create([ 'base_url' => 'https://backend.demo.taler.net/instances/sandbox', 'token' => 'Bearer token', 'client' => $httpClient ]);
Security: Timeouts and Redirect Policies (Important)
-
Request timeouts (HIGH):
- Always inject a PSR-18 client configured with explicit timeouts. Without timeouts, network calls may hang indefinitely under network partitions or server issues, causing thread/worker exhaustion and cascading failures.
- At minimum set both a connection timeout and a total request timeout. Example with Guzzle adapter above uses
connect_timeoutandtimeout.
-
Redirect handling (MEDIUM):
- Enforce an upper bound on redirect chains to avoid redirect loops and reduce SSRF blast radius. Example above uses
allow_redirects.max. - Disallow protocol downgrades to
httpby restrictingallow_redirects.protocolsto['https']. This prevents leaking credentials or tokens over plaintext during redirects.
- Enforce an upper bound on redirect chains to avoid redirect loops and reduce SSRF blast radius. Example above uses
Notes:
- PSR-18 does not define a standard for these options; use your chosen client's native configuration (e.g., Guzzle options). When using other clients, consult their documentation to apply equivalent settings (timeouts and redirect limits/HTTPS-only redirects).
Backend validation and protocol versioning
When you create a Taler instance via the Factory, the SDK proactively validates that your backend is a Merchant backend and performs a protocol version compatibility check:
- Merchant backend check (fail-fast): The
FactorycallsGET /configand validatesname === "taler-merchant". If not, it throwsInvalidArgumentExceptionduring creation. - Protocol version triplet parsing: The SDK parses the version string
current:revision:agefrom/configand compares it against the SDK's client current version (Taler::TALER_PROTOCOL_VERSION, currently 20).- Compatibility rule: clientCurrent must be within
[serverCurrent - serverAge, serverCurrent]. - If out of range, the SDK logs a WARNING via your PSR-3 logger (if provided) to help you detect potential incompatibilities early. Example log message includes server version and the supported range.
- Compatibility rule: clientCurrent must be within
Optional helpers for custom checks:
use function Taler\Helpers\parseLibtoolVersion; // returns [current, revision, age] or null use function Taler\Helpers\isProtocolCompatible; // boolean $parsed = parseLibtoolVersion('20:0:8'); // [20, 0, 8] [$serverCurrent, , $serverAge] = $parsed; $ok = isProtocolCompatible($serverCurrent, $serverAge, (int) Taler::TALER_PROTOCOL_VERSION);
Notes:
- To receive the WARNING log, pass a PSR-3 logger to the
Factory. - This check is non-fatal (only logs) as long as the backend
nameis valid; the exception is thrown only when the backend is not a merchant backend.
Payment processing (Order API)
https://docs.taler.net/core/api-merchant.html#payment-processing
The Order API provides functionality to interact with Taler order services. Here's how to use it:
Basic Setup
use Taler\Factory\Factory; $taler = Factory::create([ 'base_url' => 'https://backend.demo.taler.net/instances/sandbox', 'token' => 'Bearer token' ]); $orderClient = $taler->order();
Available Methods
Create Order
Create a new order using either a fixed-amount contract (OrderV0) or a choice-based contract (OrderV1). The call returns a PostOrderResponse with the generated order_id.
Minimal example with OrderV0 (fixed amount):
use Taler\Api\Order\Dto\OrderV0; use Taler\Api\Order\Dto\PostOrderRequest; $order = new OrderV0( summary: 'Coffee Beans 1kg', amount: 'EUR:12.50' ); $request = new PostOrderRequest(order: $order); // Create the order $result = $orderClient->createOrder($request); // Access response echo $result->order_id; // e.g., "order_123"
Example with OrderV1 (choices):
use Taler\Api\Order\Dto\OrderV1; use Taler\Api\Order\Dto\OrderChoice; use Taler\Api\Order\Dto\PostOrderRequest; $order = new OrderV1( summary: 'Monthly Subscription', choices: [ new OrderChoice(amount: 'EUR:9.99') ] ); $request = new PostOrderRequest( order: $order, ); $result = $orderClient->createOrder($request); echo $result->order_id;
Handle errors for Create Order
When creating an order, the backend may return non-200 status codes for normal error conditions. The SDK throws typed exceptions to help you handle these cases with structured DTOs:
use Taler\Api\Order\Dto\OrderV0; use Taler\Api\Order\Dto\PostOrderRequest; use Taler\Exception\OutOfStockException; // HTTP 410 use Taler\Exception\PaymentDeniedLegallyException; // HTTP 451 use Taler\Exception\TalerException; $order = new OrderV0( summary: 'Coffee Beans 1kg', amount: 'EUR:12.50' ); $request = new PostOrderRequest(order: $order); try { $response = $orderClient->createOrder($request); // $response is PostOrderResponse on success echo $response->order_id . "\n"; } catch (OutOfStockException $e) { //--- http status code 410 Gone $dto = $e->getResponseDTO(); if ($dto !== null) { // Access structured out-of-stock details // $dto->product_id (string) // $dto->requested_quantity (int) // $dto->available_quantity (int) // $dto->restock_expected?->t_s (int|string) } // Recover: show alternative products, adjust quantity, etc. } catch (PaymentDeniedLegallyException $e) { //--- http status code 451 Unavailable For Legal Reasons $dto = $e->getResponseDTO(); if ($dto !== null) { // Exchanges that denied payment // $dto->exchange_base_urls is array<int, string> } // Recover: refresh coins from these exchanges or retry with others } catch (TalerException $e) { // Other Taler API errors (e.g., 404/409). Inspect structured error details: // $error = $e->getResponseDTO(); // instance of Taler\Api\Dto\ErrorDetail or null // Or raw array JSON if preferred: $e->getResponseJson(); throw $e; } catch (\Throwable $e) { // Transport/runtime errors throw $e; }
With custom headers or raw array response:
// Custom headers $result = $orderClient->createOrder($request, [ 'X-Custom-Header' => 'value' ]); // Disable DTO wrapping to get raw array $result = $taler->config(['wrapResponse' => false]) ->order() ->createOrder($request);
Get Order Status
Retrieve the status and details of a specific order:
// Get order by ID $order = $orderClient->getOrder('order_123'); // The response type depends on the order status: if ($order instanceof CheckPaymentPaidResponse) { // Order is paid echo $order->order_status; // "paid" echo $order->deposit_total; // Total amount deposited echo $order->refunded; // Whether order was refunded echo $order->refund_pending; // Whether refund is pending echo $order->wired; // Whether funds were wired echo $order->refund_amount; // Total refunded amount // Access contract terms $terms = $order->contract_terms; echo $terms->summary; // Order summary echo $terms->order_id; // Order ID // Access last payment timestamp echo $order->last_payment->t_s; // Unix timestamp } if ($order instanceof CheckPaymentClaimedResponse) { // Order is claimed but not paid echo $order->order_status; // "claimed" echo $order->order_status_url; // Status URL for browser/wallet // Contract terms $terms = $order->contract_terms; echo $terms->summary; // Order summary echo $terms->order_id; // Order ID } if ($order instanceof CheckPaymentUnpaidResponse) { // Order is neither claimed nor paid echo $order->order_status; // "unpaid" echo $order->taler_pay_uri; // URI for wallet to process payment echo $order->summary; // Order summary echo $order->total_amount; // Total amount to pay (may be null for v1) echo $order->order_status_url; // Status URL for browser/wallet // Access creation timestamp echo $order->creation_time->t_s; // Unix timestamp } // Get order with additional parameters $order = $orderClient->getOrder('order_123', [ 'session_id' => 'session_xyz' // Optional session ID ]); // Get order with custom headers $order = $orderClient->getOrder( orderId: 'order_123', headers:[ 'X-Custom-Header' => 'value' ] );
Get Orders History
Retrieve the order history with optional filtering:
// Get orders (default limit: 20 asc) $orders = $orderClient->getOrders(); // Get orders with filters $orders = $orderClient->getOrders([ 'limit' => '-20', // last 20 orders ]); // Access order history details foreach ($orders->orders as $order) { echo $order->order_id; // Order ID of the transaction related to this entry echo $order->row_id; // Row ID of the order in the database echo $order->amount; // The amount of money the order is for echo $order->summary; // The summary of the order echo $order->refundable; // Whether the order can be refunded echo $order->paid; // Whether the order has been paid or not // Access timestamp (Unix timestamp) echo $order->timestamp->t_s; // When the order was created }
Refund Order
Initiate a refund for a specific order:
use Taler\Api\Order\Dto\RefundRequest; // Create refund request $refundRequest = new RefundRequest( refund: 'EUR:10.00', // Amount to be refunded reason: 'Customer request' // Human-readable refund justification ); // Initiate refund $refund = $orderClient->refundOrder('order_123', $refundRequest); // Access refund response details echo $refund->taler_refund_uri; // URL for wallet to process refund echo $refund->h_contract; // Contract hash for request authentication
Handle errors for Refund Order
The backend may deny a refund for legal reasons (451). The SDK raises a typed exception with structured data:
use Taler\Api\Order\Dto\RefundRequest; use Taler\Exception\PaymentDeniedLegallyException; // HTTP 451 use Taler\Exception\TalerException; $refundRequest = new RefundRequest( refund: 'EUR:10.00', reason: 'Customer request' ); try { $refund = $orderClient->refundOrder('order_123', $refundRequest); echo $refund->taler_refund_uri; } catch (PaymentDeniedLegallyException $e) { // 451 Unavailable For Legal Reasons $dto = $e->getResponseDTO(); if ($dto !== null) { // $dto->exchange_base_urls (array<int, string>) } // Recover: refresh coins from listed exchanges or retry via others } catch (TalerException $e) { // Other API errors throw $e; } catch (\Throwable $e) { // Transport/runtime errors throw $e; }
Delete Order
Delete a specific order:
// Delete an order by ID $orderClient->deleteOrder('order_123');
Forget Order
Request the backend to forget specific fields of an order's contract terms.
Notes:
- The request uses HTTP PUT and returns no content on success (HTTP 200).
- A valid JSON path must begin with
$.and end with a field identifier. Array indices and wildcards*are allowed inside the path, but the path cannot end with an index or wildcard.
use Taler\Api\Order\Dto\ForgetRequest; // Create forget request $forgetRequest = new ForgetRequest([ '$.wire_fee', '$.products[0].description', // Wildcards allowed as long as the path ends with a field '$.products[*].description' ]); // Send forget request (void on success) $orderClient->forgetOrder('order_123', $forgetRequest); // With custom headers $orderClient->forgetOrder( orderId: 'order_123', forgetRequest: $forgetRequest, headers: [ 'X-Custom-Header' => 'value' ] );
If the order or paths are invalid, a TalerException will be thrown.
Note: The delete operation returns no content on success (HTTP 204). If the order doesn't exist or can't be deleted, a TalerException will be thrown.
Asynchronous Operations
All methods support asynchronous operations with the Async suffix:
// Get orders asynchronously $ordersPromise = $orderClient->getOrdersAsync(); // Handle the promise $ordersPromise->then(function ($orders) { // Handle orders response });
Error Handling
The Order API methods may throw exceptions that you should handle:
use Taler\Exception\TalerException; try { $orders = $orderClient->getOrders(); } catch (TalerException $e) { // Handle Taler-specific errors echo $e->getMessage(); } catch (\Throwable $e) { // Handle other errors echo $e->getMessage(); }
Inspect structured error details (TalerException::getResponseDTO)
When the backend returns a JSON error response, TalerException::getResponseDTO() parses it into an ErrorDetail DTO for convenient, typed access.
use Taler\Exception\TalerException; use Taler\Api\Dto\ErrorDetail; try { $orders = $orderClient->getOrders(); } catch (TalerException $e) { /** @var ErrorDetail|null $err */ $err = $e->getResponseDTO(); if ($err !== null) { // Numeric error code unique to the condition echo $err->code . "\n"; // Human-readable hint from the server (optional) echo ($err->hint ?? '') . "\n"; // Additional optional fields may be present depending on the API // e.g., $err->detail, $err->parameter, $err->path, $err->extra } // Fallback to raw array if needed // $json = $e->getResponseJson(); }
Response Types
By default, responses are wrapped in DTOs (OrderHistory and its nested objects). You can configure this behavior:
// Disable DTO wrapping to get raw array responses $orders = $taler->config(['wrapResponse' => false])->order()->getOrders(); /* Array response example: [ 'orders' => [ [ 'order_id' => 'order_123', 'row_id' => 1, 'timestamp' => ['t_s' => 1234567890], 'amount' => '10.00', 'summary' => 'Order description', 'refundable' => true, 'paid' => true ], // ... more orders ] ] */ // Raw array response for refunds $refund = $taler->config(['wrapResponse' => false])->order()->refundOrder( 'order_123', new RefundRequest('10.00', 'Customer request') ); /* Array response example: [ 'taler_refund_uri' => 'taler://refund/...', 'h_contract' => 'hash_value' ] */
Config API
Basic Setup
use Taler\Factory\Factory; $taler = Factory::create([ 'base_url' => 'https://backend.demo.taler.net', 'token' => 'Bearer token' ]); $configClient = $taler->configApi();
Get Merchant Config
$config = $taler->configApi()->getConfig(); // MerchantVersionResponse // Core fields echo $config->version; // e.g., "42:1:0" (libtool current:revision:age) echo $config->name; // always "taler-merchant" echo $config->currency; // default currency, e.g., "EUR" // Currency specifications (map of code => CurrencySpecification) foreach ($config->currencies as $code => $spec) { echo $code; // e.g., "EUR" echo $spec->name; // e.g., "Euro" echo $spec->alt_unit_names['0']; // base symbol/name, e.g., "€" } // Trusted exchanges foreach ($config->exchanges as $ex) { // array of ExchangeConfigInfo echo $ex->base_url; // e.g., "https://exchange.example.com" echo $ex->currency; // e.g., "EUR" echo $ex->master_pub; // EddsaPublicKey as string } // Capabilities echo $config->have_self_provisioning ? 'yes' : 'no'; // bool echo $config->have_donau ? 'yes' : 'no'; // bool // Optional TAN channels (array of strings: "sms", "email") if ($config->mandatory_tan_channels !== null) { foreach ($config->mandatory_tan_channels as $ch) { echo $ch; } }
Credential Health Check
Quickly validate that your current Taler instance is properly configured and authenticated. This performs a minimal set of safe checks using the instance you already created:
- GET
/config(always) - If
tokenis non-empty: GETprivate(instance exists and is reachable) - If
tokenis non-empty: GETprivate/orders?limit=1(auth-only harmless call)
use Taler\Factory\Factory; $taler = Factory::create([ 'base_url' => 'https://backend.demo.taler.net/instances/sandbox', 'token' => 'Bearer token' ]); $diagnose = $taler->configCheck(); if ($diagnose['ok']) { // All checks passed – proceed with normal operations } else { // Inspect failing step(s): 'config', 'instance', 'auth' // Each step contains: ok (bool), status (?int), error (?string), exception (?Throwable) $failed = array_filter([ 'config' => $diagnose['config'] ?? null, 'instance' => $diagnose['instance'] ?? null, 'auth' => $diagnose['auth'] ?? null, ], fn($x) => is_array($x) && ($x['ok'] ?? false) === false); // Example: log the most relevant failure if (!empty($failed)) { $first = reset($failed); error_log('Health check failed: ' . (($first['error'] ?? 'unknown') . ' (status=' . (($first['status'] ?? null) ?? 'n/a') . ')')); // Optionally inspect original exception object for details // $ex = $first['exception'] ?? null; // instance of Taler\\Exception\\TalerException or other Throwable } }
Example report shape (abbreviated):
[
'ok' => false,
'config' => ['ok' => true, 'status' => 200, 'error' => null],
'instance' => ['ok' => true, 'status' => 200, 'error' => null],
'auth' => ['ok' => false, 'status' => 401, 'error' => 'unauthorized', 'exception' => TalerException],
]
Notes:
- If the
tokenis empty, the instance/auth checks are skipped and'ok'reflects only the/configresult. - The
exceptionfield contains the original exception object for diagnostics; avoid serializing it directly. If you need a compact form, extractclass,code,message, and optional response details.
With custom headers:
$config = $taler->configApi()->getConfig([ 'X-Custom-Header' => 'value' ]);
Raw array response (disable DTO wrapping):
$array = $taler ->config(['wrapResponse' => false]) ->configApi() ->getConfig(); // Example shape (abbreviated): // [ // 'version' => '42:1:0', // 'currency' => 'EUR', // 'currencies' => [ 'EUR' => [ 'name' => 'Euro', 'currency' => 'EUR', ... ] ], // 'exchanges' => [ [ 'base_url' => 'https://exchange.example.com', 'currency' => 'EUR', 'master_pub' => '...' ] ], // 'have_self_provisioning' => true, // 'have_donau' => false, // 'mandatory_tan_channels' => ['sms','email'] // ]
Inventory
Reference: Merchant Backend Inventory API.
Basic Setup
use Taler\Factory\Factory; $taler = Factory::create([ 'base_url' => 'https://backend.demo.taler.net/instances/sandbox', 'token' => 'Bearer token' ]); $inventory = $taler->inventory();
Categories
Get Categories
$list = $inventory->getCategories(); // CategoryListResponse foreach ($list->categories as $entry) { echo $entry->category_id; // int echo $entry->name; // string echo $entry->product_count;// int }
Get Category
$category = $inventory->getCategory(1); // CategoryProductList echo $category->name; // string foreach ($category->products as $p) { echo $p->product_id; // string }
Create Category
use Taler\Api\Inventory\Dto\CategoryCreateRequest; $req = new CategoryCreateRequest( name: 'Beverages', name_i18n: ['de' => 'Getränke'] ); $created = $inventory->createCategory($req); // CategoryCreatedResponse echo $created->category_id; // int
Update Category
use Taler\Api\Inventory\Dto\CategoryCreateRequest; $patch = new CategoryCreateRequest( name: 'Drinks' ); $inventory->updateCategory(1, $patch); // 204 No Content on success
Delete Category
$inventory->deleteCategory(1); // 204 No Content. May throw on not found.
Products
Get Products
use Taler\Api\Inventory\Dto\GetProductsRequest; $req = new GetProductsRequest(limit: 20, offset: '10'); $summary = $inventory->getProducts($req); // InventorySummaryResponse foreach ($summary->products as $entry) { echo $entry->product_id; // string echo $entry->product_serial; // int }
Get Product
$product = $inventory->getProduct('coffee-1kg'); // ProductDetail echo $product->product_name; // string echo $product->price; // Amount as string, e.g. "EUR:12.50"
Create Product
use Taler\Api\Inventory\Dto\ProductAddDetail; use Taler\Api\Dto\RelativeTime; $details = new ProductAddDetail( product_id: 'coffee-1kg', description: 'Arabica beans 1kg', unit: 'kg', price: 'EUR:12.50', total_stock: 100, product_name: 'Coffee Beans', ); $inventory->createProduct($details); // 204 No Content
Update Product
use Taler\Api\Inventory\Dto\ProductPatchDetail; $patch = new ProductPatchDetail( description: 'Arabica beans 1kg (fresh roast)', unit: 'kg', price: 'EUR:12.50', total_stock: 150 ); $inventory->updateProduct('coffee-1kg', $patch); // 204 No Content
Delete Product
$inventory->deleteProduct('coffee-1kg'); // 204 No Content. 404 may be treated as no-op.
POS Configuration
$pos = $inventory->getPos(); // FullInventoryDetailsResponse foreach ($pos->categories as $cat) { echo $cat->id . ':' . $cat->name . "\n"; } foreach ($pos->products as $p) { echo $p->product_id . ' => ' . $p->price . "\n"; }
Lock Product Quantity
Lock or unlock a quantity for a short duration.
use Taler\Api\Inventory\Dto\LockRequest; use Taler\Api\Dto\RelativeTime; $lock = new LockRequest( lock_uuid: '123e4567-e89b-12d3-a456-426614174000', duration: new RelativeTime(60_000_000), // 60 seconds quantity: 2 ); $inventory->lockProduct('coffee-1kg', $lock); // 204 No Content
Raw Array Response
Like other clients, you can disable DTO wrapping:
$array = $taler ->config(['wrapResponse' => false]) ->inventory() ->getProducts();
Asynchronous Operations
All Inventory methods support asynchronous operations using the Async suffix (e.g., getProductsAsync, getPosAsync, lockProductAsync).
Tracking Wire Transfers
Reference: Merchant Backend: GET /instances/$INSTANCE/private/transfers
The Wire Transfers API lets you list wire transfers credited to the merchant, optionally filtered via query parameters.
Basic Setup
use Taler\Factory\Factory; $taler = Factory::create([ 'base_url' => 'https://backend.demo.taler.net/instances/sandbox', 'token' => 'Bearer token' ]); $wireTransfers = $taler->wireTransfers();
Get Transfers
use Taler\Api\WireTransfers\Dto\GetTransfersRequest; // Without filters (default server-side paging) $list = $wireTransfers->getTransfers(); // TransfersList foreach ($list->transfers as $transfer) { echo $transfer->credit_amount; // e.g. "EUR:100.00" echo $transfer->wtid; // Wire transfer identifier echo $transfer->payto_uri; // Merchant payto URI echo $transfer->exchange_url; // Exchange base URL echo $transfer->transfer_serial_id; // Serial ID echo $transfer->execution_time->t_s;// Unix timestamp echo $transfer->verified ? 'yes' : 'no'; // optional echo $transfer->confirmed ? 'yes' : 'no'; // optional echo $transfer->expected ? 'yes' : 'no'; // optional } // With filters $request = new GetTransfersRequest( payto_uri: 'payto://iban/DE89370400440532013000?receiver-name=Example%20Merchant', after: '1700000000', // returns transfers after this timestamp (string per API) limit: 20, ); $filtered = $wireTransfers->getTransfers($request, [ 'X-Custom-Header' => 'value' ]);
Asynchronous
$promise = $wireTransfers->getTransfersAsync(); $promise->then(function ($list) { // $list is TransfersList when wrapResponse is true });
Raw Array Response
use Taler\Api\WireTransfers\Dto\GetTransfersRequest; $req = new GetTransfersRequest(limit: 10); $arrayResponse = $taler ->config(['wrapResponse' => false]) ->wireTransfers() ->getTransfers($req); // Example shape: // [ // 'transfers' => [ // [ // 'credit_amount' => 'EUR:10.00', // 'wtid' => 'WTID...', // 'payto_uri' => 'payto://...', // 'exchange_url' => 'https://exchange.example.com', // 'transfer_serial_id' => 123, // 'execution_time' => ['t_s' => 1700000000], // 'verified' => true, // 'confirmed' => false, // 'expected' => true // ] // ] // ]
Delete Transfer
Reference: Merchant Backend: DELETE /instances/$INSTANCE/private/transfers/$TID
// Delete by transfer serial ID (TID). 204 No Content on success. $taler->wireTransfers()->deleteTransfer('123'); // With custom headers $taler->wireTransfers()->deleteTransfer('123', [ 'X-Custom-Header' => 'value' ]);
Async variant:
$promise = $taler->wireTransfers()->deleteTransferAsync('123'); $promise->then(function () { // Deleted });
Errors raise Taler\Exception\TalerException (e.g., not found).
Bank Accounts
https://docs.taler.net/core/api-merchant.html#bank-accounts
Basic Setup
use Taler\Factory\Factory; use Taler\Api\BankAccounts\Dto\AccountAddDetails; use Taler\Api\BankAccounts\Dto\BasicAuthFacadeCredentials; $taler = Factory::create([ 'base_url' => 'https://backend.demo.taler.net/instances/sandbox', 'token' => 'Bearer token' ]); $bankAccountClient = $taler->bankAccount();
Create Bank Account
Minimal example with only a payto URI:
$details = new AccountAddDetails( payto_uri: 'payto://iban/DE89370400440532013000?receiver-name=Example%20Merchant' ); try { $response = $bankAccountClient->createAccount($details); echo "h_wire: {$response->h_wire}\n"; echo "salt: {$response->salt}\n"; } catch (\Taler\Exception\TalerException $exception) { // Handle API error }
With facade URL and Basic credentials with error handling and debug:
$details = new AccountAddDetails( payto_uri: 'payto://iban/DE89370400440532013000?receiver-name=Example%20Merchant', credit_facade_url: 'https://bank-facade.example.com/api', credit_facade_credentials: new BasicAuthFacadeCredentials('facade-user', 'facade-pass') ); try { $response = $bankAccountClient->createAccount($details); echo "h_wire: {$response->h_wire}\n"; echo "salt: {$response->salt}\n"; // dd($response); // if using Symfony VarDumper or similar } catch (\Taler\Exception\TalerException $exception) { // dd($exception->getMessage(), $exception->getCode(), $exception->getRawResponseBody()); }
Error handling follows the same pattern as other APIs and may throw Taler\Exception\TalerException.
Get Bank Accounts
Retrieve all bank accounts configured for the merchant instance. See docs: https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-accounts
try { $summary = $bankAccountClient->getAccounts(); // AccountsSummaryResponse foreach ($summary->accounts as $account) { echo $account->payto_uri . "\n"; // payto URI echo $account->h_wire . "\n"; // hash of wire details echo ($account->active ? 'active' : 'inactive') . "\n"; } } catch (\Taler\Exception\TalerException $exception) { // Handle API error }
Raw array response (disable DTO wrapping):
$summary = $taler ->config(['wrapResponse' => false]) ->bankAccount() ->getAccounts(); // Example shape: // [ // 'accounts' => [ // ['payto_uri' => 'payto://iban/..', 'h_wire' => '...', 'active' => true], // // ... // ] // ]
Get Bank Account
Retrieve a specific bank account by its h_wire. See docs: https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-accounts-$H_WIRE
$hWire = 'your-h-wire-hash'; try { $account = $bankAccountClient->getAccount($hWire); // BankAccountDetail echo $account->payto_uri . "\n"; // full payto URI echo $account->h_wire . "\n"; // hash over wire details echo $account->salt . "\n"; // salt used to compute h_wire echo ($account->active ? 'active' : 'inactive') . "\n"; echo ($account->credit_facade_url ?? ''); // optional } catch (\Taler\Exception\TalerException $exception) { // Handle API error }
Update Bank Account
Update a specific bank account by its h_wire. Returns no content on success (HTTP 204). See docs: PATCH /instances/$INSTANCE/private/accounts/$H_WIRE
Set or update the credit facade URL and credentials:
use Taler\Api\BankAccounts\Dto\AccountPatchDetails; use Taler\Api\BankAccounts\Dto\BasicAuthFacadeCredentials; $hWire = 'your-h-wire-hash'; // Provide facade URL and Basic credentials $patch = new AccountPatchDetails( credit_facade_url: 'https://bank-facade.example.com/api', credit_facade_credentials: new BasicAuthFacadeCredentials('facade-user', 'facade-pass') ); // Apply update (204 No Content on success) $bankAccountClient->updateAccount($hWire, $patch);
Remove stored facade credentials (preserving other fields):
use Taler\Api\BankAccounts\Dto\AccountPatchDetails; use Taler\Api\BankAccounts\Dto\NoFacadeCredentials; $hWire = 'your-h-wire-hash'; $patch = new AccountPatchDetails( credit_facade_credentials: new NoFacadeCredentials() ); $bankAccountClient->updateAccount($hWire, $patch);
Update only the facade URL (keep existing credentials):
use Taler\Api\BankAccounts\Dto\AccountPatchDetails; $hWire = 'your-h-wire-hash'; $patch = new AccountPatchDetails( credit_facade_url: 'https://bank-facade.example.com/v2' ); $bankAccountClient->updateAccount($hWire, $patch);
Delete Bank Account
Delete a specific bank account by its h_wire. Returns no content on success (HTTP 204). See docs: DELETE /instances/$INSTANCE/private/accounts/$H_WIRE
$hWire = 'your-h-wire-hash'; try { // 204 No Content on success $bankAccountClient->deleteAccount($hWire); } catch (\Taler\Exception\TalerException $exception) { // Handle API error }
With custom headers:
$bankAccountClient->deleteAccount( hWire: 'your-h-wire-hash', headers: [ 'X-Custom-Header' => 'value' ] );
Asynchronous Operations
All Bank Accounts methods support asynchronous operations using the Async suffix:
// Get bank accounts asynchronously $accountsPromise = $bankAccountClient->getAccountsAsync(); // Handle the promise $accountsPromise->then(function ($summary) { // $summary is an AccountsSummaryResponse when wrapResponse is true foreach ($summary->accounts as $account) { // Handle each bank account entry // $account->payto_uri, $account->h_wire, $account->active } });
OTP Devices
Reference: Merchant Backend: POST /instances/$INSTANCE/private/otp-devices
Create and register an OTP device to generate POS confirmations for orders.
Basic Setup
use Taler\Factory\Factory; $taler = Factory::create([ 'base_url' => 'https://backend.demo.taler.net/instances/sandbox', 'token' => 'Bearer token' ]); $otpDevices = $taler->otpDevices();
Create OTP Device
The API accepts the algorithm as integer or string per the docs:
- 0 or "NONE": no algorithm
- 1 or "TOTP_WITHOUT_PRICE": without amounts (typical OTP device)
- 2 or "TOTP_WITH_PRICE": with amounts (special-purpose OTP device)
On success, the endpoint returns HTTP 204 No Content.
use Taler\Api\OtpDevices\Dto\OtpDeviceAddDetails; $details = new OtpDeviceAddDetails( otp_device_id: 'pos-device-1', otp_device_description: 'Main counter POS', otp_key: 'JBSWY3DPEHPK3PXP', // Base32-encoded secret otp_algorithm: 'TOTP_WITHOUT_PRICE' // or 0|1|2 or "NONE"|"TOTP_WITH_PRICE" ); try { // 204 No Content on success $otpDevices->createOtpDevice($details); echo "OTP device created\n"; } catch (\Taler\Exception\TalerException $e) { // Handle Taler-specific API errors echo $e->getMessage(); } catch (\Throwable $e) { // Handle other errors echo $e->getMessage(); }
Update OTP Device
Reference: Merchant Backend: PATCH /instances/$INSTANCE/private/otp-devices/$DEVICE_ID
Update a registered OTP device. Returns HTTP 204 No Content on success.
use Taler\Api\OtpDevices\Dto\OtpDevicePatchDetails; $otpDevices = $taler->otpDevices(); // Minimal: update the description only (required) $patch = new OtpDevicePatchDetails( otp_device_description: 'Front desk POS' ); // Apply update (204 No Content on success) $otpDevices->updateOtpDevice('pos-device-1', $patch); // Optional: update key/algorithm/counter as well // $patch = new OtpDevicePatchDetails( // otp_device_description: 'Front desk POS', // otp_key: 'JBSWY3DPEHPK3PXP', // Base32-encoded secret // otp_algorithm: 'TOTP_WITH_PRICE', // or 0|1|2 or "NONE"|"TOTP_WITHOUT_PRICE" // otp_ctr: 0 // ); // $otpDevices->updateOtpDevice('pos-device-1', $patch);
Get OTP Device
Reference: Merchant Backend: GET /instances/$INSTANCE/private/otp-devices/$DEVICE_ID
Retrieve details of a specific OTP device.
use Taler\Api\OtpDevices\Dto\GetOtpDeviceRequest; $otpDevices = $taler->otpDevices(); // Without query parameters $device = $otpDevices->getOtpDevice('pos-device-1'); // OtpDeviceDetails echo $device->device_description; // e.g., "Main counter POS" echo $device->otp_algorithm; // e.g., 1 or "TOTP_WITHOUT_PRICE" echo $device->otp_timestamp; // Unix timestamp (int) // Optional fields (may be null) echo $device->otp_ctr ?? ''; echo $device->otp_code ?? ''; // With query parameters and custom headers $request = new GetOtpDeviceRequest( faketime: 1700000000, price: 'EUR:1.23' ); $device = $otpDevices->getOtpDevice( deviceId: 'pos-device-1', request: $request, headers: [ 'X-Custom-Header' => 'value' ] );
Raw array response (disable DTO wrapping):
$array = $taler ->config(['wrapResponse' => false]) ->otpDevices() ->getOtpDevice('pos-device-1'); // Example shape: // [ // 'device_description' => 'Main counter POS', // 'otp_algorithm' => 'TOTP_WITHOUT_PRICE', // or 0|1|2 // 'otp_timestamp' => 1700000000, // int // 'otp_ctr' => 0, // optional // 'otp_code' => '123456' // optional // ]
Get OTP Devices
Reference: Merchant Backend: GET /instances/$INSTANCE/private/otp-devices
Retrieve all registered OTP devices for the instance.
// Returns OtpDevicesSummaryResponse by default $summary = $otpDevices->getOtpDevices(); foreach ($summary->otp_devices as $device) { echo $device->otp_device_id . "\n"; // e.g., "pos-device-1" echo $device->device_description . "\n"; // e.g., "Main counter POS" } // With custom headers $summary = $otpDevices->getOtpDevices([ 'X-Custom-Header' => 'value' ]);
Raw array response (disable DTO wrapping):
$summary = $taler ->config(['wrapResponse' => false]) ->otpDevices() ->getOtpDevices(); // Example shape: // [ // 'otp_devices' => [ // [ 'otp_device_id' => 'device1', 'device_description' => 'Front desk POS' ], // [ 'otp_device_id' => 'device2', 'device_description' => 'Side counter POS' ], // ] // ]
Delete OTP Device
Reference: Merchant Backend: DELETE /instances/$INSTANCE/private/otp-devices/$DEVICE_ID
// Delete a specific OTP device by ID. 204 No Content on success. $otpDevices->deleteOtpDevice('pos-device-1'); // With custom headers $otpDevices->deleteOtpDevice('pos-device-1', [ 'X-Custom-Header' => 'value' ]);
Asynchronous
All OTP Device methods support asynchronous operations using the Async suffix:
use Taler\Api\OtpDevices\Dto\OtpDeviceAddDetails; $details = new OtpDeviceAddDetails( otp_device_id: 'pos-device-2', otp_device_description: 'Side counter POS', otp_key: 'JBSWY3DPEHPK3PXP', otp_algorithm: 1 // TOTP_WITHOUT_PRICE ); try { $promise = $otpDevices->createOtpDeviceAsync($details); // Promise resolves to null on 204 $promise->wait(); echo "OTP device created (async)\n"; } catch (\Taler\Exception\TalerException $e) { echo $e->getMessage(); } catch (\Throwable $e) { echo $e->getMessage(); }
Templates
https://docs.taler.net/core/api-merchant.html#templates
Basic Setup
use Taler\Factory\Factory; $taler = Factory::create([ 'base_url' => 'https://backend.demo.taler.net/instances/sandbox', 'token' => 'Bearer token' ]); $templates = $taler->templates();
Create Template
Returns no content on success (HTTP 204).
use Taler\Api\Templates\Dto\TemplateAddDetails; use Taler\Api\Templates\Dto\TemplateContractDetails; use Taler\Api\Dto\RelativeTime; $details = new TemplateAddDetails( template_id: 'invoice-2025', template_description: 'Default invoice template', template_contract: new TemplateContractDetails( minimum_age: 18, pay_duration: new RelativeTime(3600), summary: 'Service fee', currency: 'EUR', amount: 'EUR:10.00', ), otp_id: 'pos-device-1', editable_defaults: ['summary' => 'Editable'] ); // 204 No Content on success $templates->createTemplate($details);
Update Template
Returns no content on success (HTTP 204).
use Taler\Api\Templates\Dto\TemplatePatchDetails; use Taler\Api\Templates\Dto\TemplateContractDetails; use Taler\Api\Dto\RelativeTime; $patch = new TemplatePatchDetails( template_description: 'Updated description', template_contract: new TemplateContractDetails( minimum_age: 21, pay_duration: new RelativeTime(5400), summary: 'Updated service fee', currency: 'EUR', amount: 'EUR:12.00', ), otp_id: 'pos-device-2', editable_defaults: ['summary' => 'Editable'] ); // Apply update (204 No Content on success) $templates->updateTemplate('invoice-2025', $patch);
Get Templates
$summary = $templates->getTemplates(); // TemplatesSummaryResponse by default foreach ($summary->templates as $entry) { echo $entry->template_id . "\n"; // e.g., "invoice-2025" echo $entry->template_description . "\n"; // e.g., "Default invoice template" }
Raw array response (disable DTO wrapping):
$summaryArray = $taler ->config(['wrapResponse' => false]) ->templates() ->getTemplates(); // Example shape: // [ 'templates' => [ ['template_id' => 'invoice-2025', 'template_description' => '...'], ... ] ]
Get Template
$details = $templates->getTemplate('invoice-2025'); // TemplateDetails echo $details->template_id; // "invoice-2025" echo $details->template_description; // description // Contract defaults $contract = $details->template_contract; echo $contract->summary; // e.g., "Service fee" echo $contract->currency; // e.g., "EUR" echo $contract->amount; // e.g., "EUR:10.00" echo $contract->minimum_age; // e.g., 18 echo $contract->pay_duration->d_us; // microseconds or string representation // Optional fields echo $details->otp_id ?? ''; var_dump($details->editable_defaults ?? null);
Raw array response:
$detailsArray = $taler ->config(['wrapResponse' => false]) ->templates() ->getTemplate('invoice-2025'); // Example shape: // [ // 'template_id' => 'invoice-2025', // 'template_description' => '...', // 'template_contract' => [ 'summary' => '...', 'currency' => 'EUR', 'amount' => 'EUR:10.00', 'minimum_age' => 18, 'pay_duration' => ['d_us' => 3600000000] ], // 'otp_id' => 'pos-device-1', // 'editable_defaults' => ['summary' => 'Editable'] // ]
Delete Template
Returns no content on success (HTTP 204).
$templates->deleteTemplate('invoice-2025'); // With custom headers $templates->deleteTemplate('invoice-2025', [ 'X-Custom-Header' => 'value' ]);
Asynchronous Operations
All Templates methods support asynchronous operations with the Async suffix:
$promise = $templates->getTemplatesAsync(); $promise->then(function ($result) { // $result is TemplatesSummaryResponse when wrapResponse is true });
Token Families
The Token Families API lets you manage token families (discounts or subscriptions) for a merchant instance.
Basic Setup
use Taler\Factory\Factory; $taler = Factory::create([ 'base_url' => 'https://backend.demo.taler.net/instances/sandbox', 'token' => 'Bearer token' ]); $tokenFamilies = $taler->tokenFamilies();
Create Token Family
Create a new token family. Returns no content on success (HTTP 204).
use Taler\Api\TokenFamilies\Dto\TokenFamilyCreateRequest; use Taler\Api\Dto\Timestamp; use Taler\Api\Dto\RelativeTime; $request = new TokenFamilyCreateRequest( slug: 'family-01', name: 'My Family', description: 'Human-readable description', description_i18n: ['en' => 'Human-readable description'], // For a discount family, use expected_domains; for a subscription family, use trusted_domains extra_data: ['expected_domains' => ['example.com']], valid_after: new Timestamp(1700000000), valid_before: new Timestamp(1800000000), // Ensure duration >= validity_granularity + start_offset duration: new RelativeTime(60_000_000), // 1 minute validity_granularity: new RelativeTime(60_000_000),// 1 minute start_offset: new RelativeTime(0), kind: 'discount' // or 'subscription' ); // 204 No Content on success $tokenFamilies->createTokenFamily($request);
Update Token Family
Update an existing token family. Returns detailed information (HTTP 200) as TokenFamilyDetails.
use Taler\Api\TokenFamilies\Dto\TokenFamilyUpdateRequest; use Taler\Api\Dto\Timestamp; $patch = new TokenFamilyUpdateRequest( name: 'Updated Name', description: 'Updated Description', description_i18n: ['en' => 'Updated Description'], // Depends on the token family kind; adjust accordingly extra_data: ['trusted_domains' => ['example.com']], valid_after: new Timestamp(1700000100), valid_before: new Timestamp(1800000100) ); // Returns TokenFamilyDetails on HTTP 200 $details = $tokenFamilies->updateTokenFamily('family-01', $patch);
Get Token Families
List all configured token families for the instance. Returns TokenFamiliesList (HTTP 200).
$list = $tokenFamilies->getTokenFamilies(); // TokenFamiliesList foreach ($list->token_families as $family) { echo $family->slug . "\n"; // e.g., "family-01" echo $family->name . "\n"; // human-readable name echo $family->kind . "\n"; // "discount" or "subscription" echo $family->valid_after->t_s . "\n";// Unix timestamp echo $family->valid_before->t_s . "\n";// Unix timestamp }
Get Token Family
Get detailed information about a specific token family. Returns TokenFamilyDetails (HTTP 200).
$details = $tokenFamilies->getTokenFamily('family-01'); // TokenFamilyDetails echo $details->slug; // e.g., "family-01" echo $details->name; // human-readable name echo $details->description; // description echo $details->kind; // "discount" or "subscription" echo $details->issued; // number of tokens issued echo $details->used; // number of tokens used echo $details->valid_after->t_s; // Unix timestamp echo $details->valid_before->t_s; // Unix timestamp echo $details->duration->d_us; // microseconds echo $details->validity_granularity->d_us;// microseconds echo $details->start_offset->d_us; // microseconds // Optional var_dump($details->description_i18n ?? null); var_dump($details->extra_data ?? null);
Delete Token Family
Delete a specific token family. Returns no content on success (HTTP 204).
$tokenFamilies->deleteTokenFamily('family-01'); // With custom headers $tokenFamilies->deleteTokenFamily('family-01', [ 'X-Custom-Header' => 'value' ]);
Asynchronous Operations
Every Token Families method also supports an async variant using the Async suffix (e.g., getTokenFamiliesAsync, getTokenFamilyAsync, createTokenFamilyAsync, updateTokenFamilyAsync, deleteTokenFamilyAsync).
// Example: Get token families asynchronously $promise = $taler->tokenFamilies()->getTokenFamiliesAsync(); $promise->then(function ($result) { // $result is TokenFamiliesList when wrapResponse is true });
Webhooks
https://docs.taler.net/core/api-merchant.html#webhooks
Basic Setup
use Taler\Factory\Factory; $taler = Factory::create([ 'base_url' => 'https://backend.demo.taler.net/instances/sandbox', 'token' => 'Bearer token' ]); $webhooks = $taler->webhooks();
Create Webhook
Returns no content on success (HTTP 204).
use Taler\Api\Webhooks\Dto\WebhookAddDetails; $details = new WebhookAddDetails( webhook_id: 'checkout-completed', event_type: 'ORDER_PAID', url: 'https://example.com/webhooks/order-paid', http_method: 'POST', header_template: '{"X-Webhook":"paid"}', // optional body_template: '{"order_id":"$order_id"}' // optional ); // 204 No Content on success $webhooks->createWebhook($details);
Update Webhook
Returns no content on success (HTTP 204).
use Taler\Api\Webhooks\Dto\WebhookPatchDetails; $patch = new WebhookPatchDetails( event_type: 'ORDER_PAID', url: 'https://example.com/webhooks/order-paid-v2', http_method: 'POST', header_template: '{"X-Webhook":"paid"}', // optional body_template: '{"order_id":"$order_id"}' // optional ); $webhooks->updateWebhook('checkout-completed', $patch);
Get Webhooks
$summary = $webhooks->getWebhooks(); // WebhookSummaryResponse foreach ($summary->webhooks as $entry) { echo $entry->webhook_id . "\n"; // e.g., "checkout-completed" echo $entry->event_type . "\n"; // e.g., "ORDER_PAID" } // With custom headers $summary = $webhooks->getWebhooks([ 'X-Custom-Header' => 'value' ]);
Get Webhook
$details = $webhooks->getWebhook('checkout-completed'); // WebhookDetails echo $details->event_type; // e.g., "ORDER_PAID" echo $details->url; // e.g., "https://example.com/webhooks/order-paid" echo $details->http_method; // e.g., "POST" echo $details->header_template ?? ''; // optional echo $details->body_template ?? ''; // optional
Delete Webhook
Returns no content on success (HTTP 204).
$webhooks->deleteWebhook('checkout-completed'); // With custom headers $webhooks->deleteWebhook('checkout-completed', [ 'X-Custom-Header' => 'value' ]);
Asynchronous Operations
Every Webhooks method also supports an async variant using the Async suffix (e.g., createWebhookAsync, updateWebhookAsync, getWebhooksAsync, getWebhookAsync, deleteWebhookAsync).
use Taler\Api\Webhooks\Dto\WebhookAddDetails; $details = new WebhookAddDetails( webhook_id: 'checkout-completed', event_type: 'ORDER_PAID', url: 'https://example.com/webhooks/order-paid', http_method: 'POST' ); $promise = $taler->webhooks()->createWebhookAsync($details); // Promise resolves to null on 204 No Content $promise->then(function () { echo "Webhook created (async)\n"; });
Donau Charity
Reference: Merchant Backend Donau Charity endpoints.
Basic Setup
use Taler\Factory\Factory; $taler = Factory::create([ 'base_url' => 'https://backend.demo.taler.net/instances/sandbox', 'token' => 'Bearer token' ]); $donau = $taler->donauCharity();
Get Linked Donau Charity Instances
// Returns DonauInstancesResponse by default $response = $donau->getInstances(); foreach ($response->donau_instances as $donau) { echo $donau->donau_instance_serial . "\n"; // int echo $donau->charity_name . "\n"; // string echo $donau->donau_url . "\n"; // string (base URL) echo $donau->charity_id . "\n"; // int echo $donau->charity_pub_key . "\n"; // EddsaPublicKey as string echo $donau->charity_max_per_year . "\n"; // Amount as string, e.g. "EUR:1000" echo $donau->charity_receipts_to_date . "\n"; // Amount as string echo $donau->current_year . "\n"; // int }
Raw array response (disable DTO wrapping):
$array = $taler ->config(['wrapResponse' => false]) ->donauCharity() ->getInstances();
Link (Create) a Donau Charity
Returns no content on success (HTTP 204). When the backend requires MFA (since v21), a Challenge is returned (HTTP 202).
use Taler\Api\DonauCharity\Dto\PostDonauRequest; $request = new PostDonauRequest( donau_url: 'https://donau.example', // https base URL of the Donau service charity_id: 7 // numeric identifier within the Donau service ); $challenge = $donau->createDonauCharity($request); // null on 204; Challenge on 202 (optional)
Unlink (Delete) a Donau Charity by Serial
Returns no content on success (HTTP 204). If the serial does not exist, a TalerException is thrown.
// Delete by Donau instance serial $donau->deleteDonauCharityBySerial(321); // With custom headers $donau->deleteDonauCharityBySerial(321, [ 'X-Custom-Header' => 'value' ]);
Instance Management
Manage merchant instances and access tokens. Reference: Merchant Backend Instance API.
Basic Setup
use Taler\Factory\Factory; $taler = Factory::create([ 'base_url' => 'https://backend.demo.taler.net/instances/sandbox', 'token' => 'Bearer token' ]); $instances = $taler->instance();
Create Instance
use Taler\Api\Instance\Dto\InstanceConfigurationMessage; use Taler\Api\Instance\Dto\InstanceAuthConfigToken; use Taler\Api\Dto\Location; use Taler\Api\Dto\RelativeTime; $config = new InstanceConfigurationMessage( id: 'shop-1', name: 'My Shop', auth: new InstanceAuthConfigToken(password: 'super-secret'), address: new Location(country: 'DE', town: 'Berlin'), jurisdiction: new Location(country: 'DE', town: 'Berlin'), use_stefan: true, default_wire_transfer_delay: new RelativeTime(3600_000_000), // 1 hour default_pay_delay: new RelativeTime(300_000_000), // 5 minutes email: 'merchant@example.com' ); // 204 No Content on success (no return value) $instances->createInstance($config);
Update Instance
use Taler\Api\Instance\Dto\InstanceReconfigurationMessage; use Taler\Api\Dto\Location; use Taler\Api\Dto\RelativeTime; $patch = new InstanceReconfigurationMessage( name: 'My Shop GmbH', address: new Location(country: 'DE', town: 'Berlin'), jurisdiction: new Location(country: 'DE', town: 'Berlin'), use_stefan: true, default_wire_transfer_delay: new RelativeTime(7200_000_000), // 2 hours default_pay_delay: new RelativeTime(600_000_000), // 10 minutes website: 'https://shop.example.com' ); // 204 No Content on success $instances->updateInstance('shop-1', $patch);
Get Instances
$list = $instances->getInstances(); // InstancesResponse foreach ($list->instances as $i) { echo $i->id . ' => ' . $i->name . "\n"; }
Get Instance
$details = $instances->getInstance('shop-1'); // QueryInstancesResponse echo $details->name; // e.g., "My Shop GmbH" echo $details->merchant_pub; // EddsaPublicKey echo $details->default_pay_delay->d_us; // microseconds
Authentication & Access Tokens
Request a login token for an instance and list or revoke tokens.
use Taler\Api\Instance\Dto\LoginTokenRequest; use Taler\Api\Dto\RelativeTime; // Request a login token (Authorization header value) $req = new LoginTokenRequest( scope: 'order-full', duration: new RelativeTime(3_600_000_000), description: 'Backoffice session' ); $token = $instances->getAccessToken('shop-1', $req); // LoginTokenSuccessResponse echo $token->access_token; // RFC 8959 prefix included // List issued tokens (latest first by default) use Taler\Api\Instance\Dto\GetAccessTokensRequest; $tokens = $instances->getAccessTokens('shop-1', new GetAccessTokensRequest(limit: -20)); if ($tokens !== null) { foreach ($tokens->tokens as $t) { echo $t->serial . ' ' . $t->scope . "\n"; } } // Revoke the token presented in the Authorization header $instances->deleteAccessToken('shop-1'); // 204 No Content // Revoke a token by its serial number $instances->deleteAccessTokenBySerial('shop-1', 123); // 204 No Content
Alternatively, you can let the SDK manage access tokens automatically via the Factory. Provide username, password, and instance to Factory::create(...) (optionally scope, duration_us, description) and the SDK will obtain and refresh the token for you. See “Factory-managed authentication” in the Usage section above.
KYC Status
use Taler\Api\Instance\Dto\GetKycStatusRequest; $kyc = $instances->getKycStatus('shop-1', new GetKycStatusRequest( h_wire: 'H_WIRE_HASH', lpt: 3, timeout_ms: 30_000 )); if ($kyc !== null) { // MerchantAccountKycRedirectsResponse foreach ($kyc->kyc_redirects as $r) { echo $r->exchange_url . "\n"; } }
Merchant Statistics
use Taler\Api\Instance\Dto\GetMerchantStatisticsAmountRequest; use Taler\Api\Instance\Dto\GetMerchantStatisticsCounterRequest; $amounts = $instances->getMerchantStatisticsAmount('shop-1', 'ORDERS', new GetMerchantStatisticsAmountRequest(by: 'BUCKET')); echo $amounts->by_bucket[0]->amount; // e.g., "EUR:10.00" $counters = $instances->getMerchantStatisticsCounter('shop-1', 'VISITS', new GetMerchantStatisticsCounterRequest(by: 'INTERVAL')); echo $counters->by_interval[0]->count; // integer
Delete or Purge Instance
Disable or permanently purge an instance. When 2FA is required, a Challenge is returned (HTTP 202).
// Disable instance (204 on success, or Challenge on 202) $challenge = $instances->deleteInstance('shop-1'); if ($challenge instanceof Taler\Api\Instance\Dto\Challenge) { echo $challenge->getChallengeId(); } // Purge instance (irreversible) $instances->deleteInstance('shop-1', purge: true);
Raw Array Responses
Like other clients, you can disable DTO wrapping:
$array = $taler ->config(['wrapResponse' => false]) ->instance() ->getInstances();
Asynchronous Operations
All Instance methods also support asynchronous operations using the Async suffix.
$promise = $taler->instance()->getInstancesAsync(); $promise->then(function ($result) { // $result is InstancesResponse when wrapResponse is true });
Two Factor Auth (2FA)
Certain protected operations (for example “update auth”, “change bank account”, or “delete instance”) may require a two-factor authentication. In such cases, the protected endpoint replies with HTTP 202 and a Challenge ID. You then:
- Request transmission of a TAN for that Challenge
- Confirm the Challenge with the received TAN
- Repeat the original protected request with the exact same body and the
Taler-Challenge-Idsheader set to the Challenge ID
Basic setup
use Taler\Factory\Factory; use Taler\Api\Instance\Dto\InstanceAuthConfigToken; use Taler\Api\TwoFactorAuth\Dto\MerchantChallengeSolveRequest; $taler = Factory::create([ 'base_url' => 'https://backend.demo.taler.net', 'token' => 'Bearer token' ]); $instances = $taler->instance(); $twofa = $taler->twoFactorAuth();
1) Protected call returns Challenge (example: update auth)
// Example protected request that may require 2FA $authConfig = new InstanceAuthConfigToken(password: 'new-secret'); // If 2FA is required, the method returns a Challenge (HTTP 202). // If successful without 2FA, it returns null (HTTP 204). $challenge = $instances->updateAuth('shop-1', $authConfig); if ($challenge !== null) { $challengeId = $challenge->getChallengeId(); }
2) Request TAN for the challenge
// The body must be a JSON object, but can be empty; the SDK sends {} by default. $response = $twofa->requestChallenge('shop-1', $challengeId); // $response is ChallengeRequestResponse with timing guidance: // $response->solve_expiration->t_s // $response->earliest_retransmission->t_s
3) Confirm the challenge with the TAN
// TAN received out-of-band (e.g., SMS/email/UI input) $tan = $merchantProvidedTan; $solve = new MerchantChallengeSolveRequest($tan); $twofa->confirmChallenge('shop-1', $challengeId, $solve); // 204 No Content on success
4) Repeat the original protected call with Taler-Challenge-Ids header
// IMPORTANT: The repeated request must exactly match the original request body. // Provide the challenge ID as a header when repeating the operation: $instances->updateAuth( instanceId: 'shop-1', authConfig: $authConfig, headers: ['Taler-Challenge-Ids' => $challengeId] ); // 204 on success
Notes:
- Other protected operations (e.g.,
deleteInstance) follow the same pattern: try the operation, handle 202 Challenge, request TAN, confirm, then retry the operation with theTaler-Challenge-Idsheader. - All 2FA-related request/response DTOs use SDK-wide conventions (e.g., factory
createFromArray, request DTOs validate, response DTOs do not validate). - All actions also have methods suffixed with
Asyncto run operations asynchronously.
Logging
The TalerPHP SDK supports logging through PSR-3 LoggerInterface. You can provide your own PSR-3 compatible logger (like Monolog) for logging of API interactions.
Basic Logging Setup
use Monolog\Logger; use Monolog\Handler\StreamHandler; // Create a Monolog logger $logger = new Logger('taler'); $logger->pushHandler(new StreamHandler('path/to/your.log', Logger::DEBUG)); // Initialize Taler with logger (and enable SDK debug logging) $taler = Factory::create([ 'base_url' => 'https://backend.demo.taler.net/instances/sandbox', 'token' => 'Bearer token', 'logger' => $logger, 'debugLoggingEnabled' => true ]);
What Gets Logged
The SDK logs the following information:
- HTTP request details (URL, method, headers)
- HTTP response details (status code, sanitized headers, sanitized body preview)
- Error conditions and exceptions
- API operation failures
Notes:
- Logging is performed only when
debugLoggingEnabledistrue. Otherwise, the SDK does not execute any logging code paths (zero overhead). - Request body logging is disabled.
- Response body logging is sanitized and truncated:
- Secrets (e.g., Authorization tokens, access_token, api_key, client_secret, password) are redacted.
- Sensitive headers such as Authorization, Cookie, and Set-Cookie are redacted.
- URL userinfo (user:pass@) is redacted in logs.
- Sensitive query parameters in URLs (e.g.,
authorization,access_token,token,api_key,api-key,client_secret,password,pwd,merchant_sig,lpt) are redacted. - URL-bearing headers like
LocationandContent-Locationare sanitized (including query redaction). - Only a preview (up to ~4KB) is logged; non-seekable streams are skipped.
Log Levels Used
- DEBUG: Detailed request/response information
- ERROR: API errors, request failures, and exceptions
Performance note:
- Enabling DEBUG logging increases overhead due to header redaction and response body sanitization. For large responses this can be noticeable. If you do not need logging, do not provide a logger and keep
debugLoggingEnabledasfalse(default).
Toggle at Runtime
You can enable or disable SDK debug logging at runtime:
// Enable $taler->config(['debugLoggingEnabled' => true]); // Disable $taler->config(['debugLoggingEnabled' => false]);
Example Log Output
[2024-03-21 10:15:30] taler.DEBUG: Taler request: https://backend.demo.taler.net/instances/sandbox/config, GET
[2024-03-21 10:15:30] taler.DEBUG: Taler request headers: {"User-Agent":["Mirrorps_Taler_PHP"],"Authorization":["Bearer token"]}
[2024-03-21 10:15:31] taler.DEBUG: Taler response: 200, OK
[2024-03-21 10:15:31] taler.DEBUG: Taler response headers: {"Content-Type":["application/json"]}
[2024-03-21 10:15:31] taler.DEBUG: Taler response body: {"version":"0.8.0","name":"taler-exchange"}
Custom Logger
If you want to implement your own logger, it must implement Psr\Log\LoggerInterface.
Caching
The TalerPHP SDK supports caching through PSR-16 SimpleCache interface. You can provide any PSR-16 compatible cache implementation to store API responses and reduce unnecessary network requests.
Basic Cache Setup
use Symfony\Component\Cache\Adapter\FilesystemAdapter; use Symfony\Component\Cache\Psr16Cache; // Create a PSR-16 cache implementation $filesystemAdapter = new FilesystemAdapter(); $cache = new Psr16Cache($filesystemAdapter); // Initialize Taler with cache $taler = Factory::create([ 'base_url' => 'https://backend.demo.taler.net/instances/sandbox', 'token' => 'Bearer token', 'cache' => $cache ]);
Using Cache with API Calls
You can enable caching for specific API calls using the cache() method. The method accepts:
minutes: How long to cache the response (in minutes)cacheKey: (Optional) Custom cache key. If not provided, one will be generated automatically
Example: Caching Merchant Backend Configuration
// Cache the merchant backend configuration for 60 minutes $config = $taler->cache(60) ->configApi() ->getConfig(); // Use a custom cache key $config = $taler->cache(60, 'merchant_config') ->configApi() ->getConfig(); // Subsequent calls within the TTL will return cached data $cachedConfig = $taler->configApi()->getConfig(); // Force delete cached data $taler->cacheDelete('merchant_config');
Cache Implementation Requirements
If you want to implement your own cache, it must implement Psr\SimpleCache\CacheInterface.
Security notes
-
Do not enable DEBUG logging in production
- Keep
debugLoggingEnabledset tofalsein production. If you don’t need SDK logs, omit theloggerentirely for zero overhead.
- Keep
-
Never include credentials in the base URL
- Avoid userinfo (user:pass@) and secrets in query strings. Pass tokens via headers (e.g.,
Authorization) and configuration.
- Avoid userinfo (user:pass@) and secrets in query strings. Pass tokens via headers (e.g.,
-
Configure timeouts, TLS verification, and redirect policy in the injected HTTP client
- Always set
timeoutandconnect_timeout. Ensure TLS verification is ON (e.g., Guzzleverify => trueor a CA bundle path). Limit redirects and restrict to HTTPS only (see example above).
- Always set
-
Avoid caching responses containing sensitive data
- If you use the PSR-16 cache integration, do not cache endpoints that may include tokens. Prefer short TTLs and explicit cache keys; clear cached entries promptly when no longer needed.
Using TalerPHP in WordPress
You can integrate the SDK into WordPress either from a theme or (recommended) from a dedicated plugin.
Install the SDK with Composer
Install mirrorps/taler-php in a location that WordPress can autoload:
cd wp-plugins mkdir taler-payments && cd taler-payments composer require mirrorps/taler-php
Make sure your plugin (or theme) loads Composer’s autoloader:
// In plugins/taler-payments/taler-payments.php require_once __DIR__ . '/vendor/autoload.php';
If your Composer setup lives elsewhere (for example at the WordPress root), adjust the path accordingly (e.g. require_once ABSPATH . '../vendor/autoload.php';).
Store secrets such as the backend URL and access token in environment variables or wp-config.php (constants), not in your plugin code.
Minimal plugin: shortcode-based “Pay with Taler” button
Create a plugin file at plugins/taler-payments/taler-payments.php:
<?php /** * Plugin Name: Taler Payments * Description: Simple integration of the TalerPHP SDK. * Version: 0.1.0 */ if (! defined('ABSPATH')) { exit; } // Adjust the path if your composer.json is located elsewhere. require_once __DIR__ . '/vendor/autoload.php'; use Taler\Factory\Factory; use Taler\Api\Order\Dto\OrderV0; use Taler\Api\Order\Dto\PostOrderRequest; use Taler\Api\Order\Dto\CheckPaymentUnpaidResponse; /** * Lazily create and reuse a Taler client. */ function taler_wp_client(): \Taler\Taler { static $client = null; if ($client === null) { // Configure via environment variables or wp-config.php constants. $baseUrl = getenv('TALER_BASE_URL') ?: (defined('TALER_BASE_URL') ? TALER_BASE_URL : ''); $token = getenv('TALER_TOKEN') ?: (defined('TALER_TOKEN') ? TALER_TOKEN : ''); $client = Factory::create([ 'base_url' => $baseUrl, 'token' => $token, // e.g. "Bearer abc..." ]); } return $client; } /** * Shortcode: [taler_pay_button amount="EUR:5.00" summary="Donation"] * * Renders a “Pay with GNU Taler” link that the wallet can use. */ function taler_wp_render_pay_button($atts): string { $atts = shortcode_atts( [ 'amount' => 'EUR:5.00', 'summary' => 'Donation', ], $atts, 'taler_pay_button' ); $taler = taler_wp_client(); $orderClient = $taler->order(); $order = new OrderV0( summary: sanitize_text_field($atts['summary']), amount: sanitize_text_field($atts['amount']), fulfillment_message: 'Thank you for your purchase. Your order will be fulfilled after payment.' ); $request = new PostOrderRequest(order: $order); try { // 1) Create order and get its ID $created = $orderClient->createOrder($request); // 2) Fetch unpaid order status, including taler_pay_uri $status = $orderClient->getOrder($created->order_id); if ($status instanceof CheckPaymentUnpaidResponse && $status->taler_pay_uri !== null) { return sprintf( '<a href="%s" class="taler-pay-button">Pay with GNU Taler</a>', $status->taler_pay_uri ); } return '<!-- Taler: order created but no unpaid status/pay URI available. -->'; } catch (\Taler\Exception\TalerException $e) { if (defined('WP_DEBUG') && WP_DEBUG) { return '<!-- Taler error: ' . esc_html($e->getMessage()) . ' -->'; } return '<!-- Taler payment temporarily unavailable. -->'; } catch (\Throwable $e) { return '<!-- Taler runtime error. -->'; } } add_shortcode('taler_pay_button', 'taler_wp_render_pay_button');
Usage inside posts, pages, or blocks:
[taler_pay_button amount="EUR:12.50" summary="Coffee Beans 1kg"]
The shortcode:
-
Creates a Taler order using the SDK’s Order API.
-
Retrieves the unpaid status to obtain the
taler_pay_uri. -
Renders a link that compatible Taler wallets can use to start the payment flow. You can further adapt this example to:
-
Redirect to a custom thank-you page after payment confirmation using the Order status API.
-
Log or persist WordPress-side order metadata alongside the Taler
order_id. -
Add styling for
.taler-pay-buttonvia your theme or plugin CSS.
Using TalerPHP in Drupal
You can integrate the SDK into Drupal (9/10/11) via a small custom module that uses the Drupal service container.
Install the SDK with Composer
From your Drupal project root (where composer.json lives):
cd /path/to/drupal
composer require mirrorps/taler-php
Drupal will automatically pick up the SDK through Composer’s autoloader; no manual require is needed.
Store secrets such as the backend URL and access token in environment variables (.env) or settings.php, not directly in module code.
Create a minimal taler_payments module
Create the module directory:
mkdir -p web/modules/custom/taler_payments
Or, if your Drupal root is not web/, adjust the path accordingly (for example modules/custom/taler_payments).
1) Module info file
Create web/modules/custom/taler_payments/taler_payments.info.yml:
name: 'Taler Payments' type: module description: 'Simple integration of the TalerPHP SDK.' core_version_requirement: '^9 || ^10 || ^11' package: 'Payments'
2) Service definition: shared Taler client
Create web/modules/custom/taler_payments/taler_payments.services.yml:
services: taler_payments.client: class: Taler\Taler factory: ['Drupal\taler_payments\Factory\TalerClientFactory', 'create']
Now create the factory at web/modules/custom/taler_payments/src/Factory/TalerClientFactory.php:
<?php namespace Drupal\taler_payments\Factory; use Taler\Factory\Factory; use Taler\Taler; /** * Factory for creating the shared Taler client used in Drupal. * * This reads configuration from environment variables so we don't rely on * Symfony-style %env()% placeholders in Drupal's container. */ final class TalerClientFactory { /** * Create the Taler client instance. */ public static function create(): Taler { $baseUrl = getenv('TALER_BASE_URL') ?: 'https://backend.demo.taler.net/instances/sandbox'; $token = getenv('TALER_TOKEN') ?: ''; return Factory::create([ 'base_url' => $baseUrl, 'token' => $token, ]); } }
Configure environment
Make sure your web/PHP environment exports:
TALER_BASE_URL– your Taler backend instance (e.g. sandbox)TALER_TOKEN– your bearer token for that instance, e.g.Bearer <real-token>
3) Route for a “Pay with Taler” page
Create web/modules/custom/taler_payments/taler_payments.routing.yml:
taler_payments.pay_page: path: '/taler/pay' defaults: _controller: '\Drupal\taler_payments\Controller\TalerPayController::pay' _title: 'Pay with GNU Taler' requirements: _permission: 'access content'
4) Allow the taler:// URI scheme
By default, Drupal considers only a few URI schemes safe; taler:// is not one of them. Add a small .module file to allow it.
Create web/modules/custom/taler_payments/taler_payments.module:
<?php /** * @file * Hooks and callbacks for the Taler Payments module. */ /** * Implements hook_allowed_protocols_alter(). * * Ensure Drupal treats the "taler" URI scheme as safe so that links to * taler://pay/... are not stripped or rewritten. */ function taler_payments_allowed_protocols_alter(array &$protocols): void { if (!in_array('taler', $protocols, TRUE)) { $protocols[] = 'taler'; } }
5) Controller that uses the SDK
Create web/modules/custom/taler_payments/src/Controller/TalerPayController.php:
<?php namespace Drupal\taler_payments\Controller; use Drupal\Core\Controller\ControllerBase; use Symfony\Component\DependencyInjection\ContainerInterface; use Taler\Taler; use Taler\Api\Order\Dto\OrderV0; use Taler\Api\Order\Dto\PostOrderRequest; use Taler\Api\Order\Dto\CheckPaymentUnpaidResponse; class TalerPayController extends ControllerBase { /** * @var \Taler\Taler */ protected Taler $taler; public function __construct(Taler $taler) { $this->taler = $taler; } public static function create(ContainerInterface $container): self { return new self( $container->get('taler_payments.client') ); } /** * Simple "Pay with GNU Taler" page. */ public function pay(): array { $orderClient = $this->taler->order(); $order = new OrderV0( summary: 'Donation', amount: 'KUDOS:5.00', // adjust to your backend configuration fulfillment_message: 'Thank you for your donation. Your order will be fulfilled after payment.' ); $request = new PostOrderRequest(order: $order); try { // 1) Create order and get its ID. $created = $orderClient->createOrder($request); // 2) Fetch unpaid order status, including taler_pay_uri. $status = $orderClient->getOrder($created->order_id); if ($status instanceof CheckPaymentUnpaidResponse && $status->taler_pay_uri !== null) { // Render a direct taler:// link without going through Drupal's Url // validators, since taler:// is a special wallet URI. $uri = $status->taler_pay_uri; $safe_uri = htmlspecialchars($uri, ENT_QUOTES, 'UTF-8'); $link = '<a href="' . $safe_uri . '" class="taler-pay-button">' . $this->t('Pay with GNU Taler') . '</a>'; // Mark the link as safe, since it comes from a trusted Taler backend. return [ '#markup' => Markup::create($link), ]; } return [ '#markup' => $this->t('Taler order created, but no unpaid status is available.'), ]; } catch (\Taler\Exception\TalerException $e) { // In production, you might log this instead of showing details. return [ '#markup' => $this->t('Taler payment temporarily unavailable.'), ]; } catch (\Throwable $e) { return [ '#markup' => $this->t('An unexpected error occurred.'), ]; } } }
6) Enable and test
- Clear caches (via Drush or UI):
- UI:
Admin → Configuration → Development → Performanceand click “Clear all caches”.
- Enable the module:
- Go to
Admin → Extend. - Enable “Taler Payments” (under “Payments”).
- Test the payment page:
-
Visit:
/taler/pay - With valid
TALER_BASE_URL,TALER_TOKEN, and the correctamountcurrency:- The controller should:
- Create an order via the TalerPHP SDK.
- Fetch its unpaid status (including
taler_pay_uri). - Render a “Pay with GNU Taler” link with a
taler://pay/...URI, which a Taler wallet can use.
- The controller should:
If you see an error like Unexpected response status code: 401, this indicates the Taler backend rejected the request (e.g. invalid token); double-check TALER_TOKEN and TALER_BASE_URL configuration.
Running Tests
To run the test suite:
composer install
php vendor/bin/phpunit
Contributing
Contributions are welcome! Please see CONTRIBUTING.md for guidelines.
Security Note: SSRF Risk
The TalerPHP SDK allows developers to make HTTP requests to arbitrary endpoints based on configuration and API usage. If your application accepts or processes user-provided endpoints, you should be aware of the risk of Server-Side Request Forgery (SSRF).
License
This project is licensed under the MIT License. See LICENSE for details.
Support
If you have questions or need help, open an issue or start a discussion on the repository.
Acknowledgments
- GNU Taler
- All contributors and the open source community
Funding
This project is funded through NGI TALER Fund, a fund established by NLnet with financial support from the European Commission's Next Generation Internet program. Learn more at the NLnet project page.