mirrorps / taler-php
PHP SDK for GNU Taler REST API
Installs: 8
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)
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.
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 PATCH and returns no content on success (HTTP 200 or 204).
- 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, or if the backend rejects the request (for example HTTP 400, 404, or 409), a TalerException will be thrown. The exception code contains the HTTP status, and you can inspect structured error details via $e->getResponseDTO() if the backend returned a JSON error body.
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 no content on success (HTTP 204).
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) ); // 204 No Content on success $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.