kemo / monri-client
Monri payment gateway PHP client
Requires
- php: >=8.1
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.0
- guzzlehttp/guzzle: ^7.0
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^10.0 || ^11.0
- squizlabs/php_codesniffer: ^3.0
README
PHP client for the Monri payment gateway. Supports PHP 8.1+.
Installation
composer require kemo/monri-client
Configuration
use Kemo\Monri\Config; use Kemo\Monri\Environment; use Kemo\Monri\MonriClient; $client = new MonriClient(new Config( merchantKey: 'your-merchant-key', authenticityToken: 'your-authenticity-token', environment: Environment::Production, // or Environment::Test ));
Or from environment variables (MONRI_MERCHANT_KEY, MONRI_AUTHENTICITY_TOKEN, MONRI_ENVIRONMENT):
$client = MonriClient::fromEnv();
Both credentials are available in the Monri merchant dashboard.
Transactions (capture, refund, void)
The XML transaction-management API is available via transactions():
$result = $client->transactions()->refund('order-123', 5000, 'BAM'); $result->isApproved(); // true, otherwise ApiException is thrown
Authentication
All requests are signed using the documented WP3-v2.1 digest scheme:
Authorization: WP3-v2.1 {authenticity_token} {timestamp} {SHA512(key+timestamp+token+path+body)}
This is handled automatically — no manual token management required.
Payments
Create a payment
$payment = $client->payments()->create([ 'order_number' => 'ORD-' . uniqid(), 'amount' => 5000, // minor units (5000 = €50.00) 'currency' => 'EUR', 'order_info' => 'GOD Club membership', 'transaction_type' => 'purchase', // or 'authorize' ]); echo $payment->id; // payment ID to pass to the frontend SDK echo $payment->clientSecret; // client secret for frontend SDK echo $payment->status; // 'approved' | 'invalid-request' | 'error'
Optional params: scenario, customer_uuid, supported_payment_methods, success_url_override, cancel_url_override, callback_url_override.
Update a payment (change amount before capture)
$payment = $client->payments()->update('pay_123', ['amount' => 3000]);
Check payment status
$status = $client->payments()->status('pay_123'); echo $status->status; // 'approved' | 'invalid-request' | 'error' echo $status->paymentStatus; // e.g. 'completed' if ($status->result !== null) { echo $status->result->amount; // int, minor units echo $status->result->currency; // 'EUR' echo $status->result->orderNumber; }
Verify a webhook callback
Monri signs every server-side callback with Authorization: WP3-callback {digest}
where digest = sha512(merchant_key + raw_body). Always verify before trusting
the payload:
$payload = $client->callbacks()->parse( $request->getContent(), // raw body, exactly as received $request->headers->get('Authorization'), // full header value ); // throws AuthenticationException on bad signature, MonriException on bad JSON if ($payload->isApproved()) { echo $payload->orderNumber; echo $payload->amount; // int, minor units echo $payload->approvalCode; echo $payload->maskedPan; }
Use verify() instead of parse() when you only need a boolean check:
if (!$client->callbacks()->verify($rawBody, $authorizationHeader)) { // reject the request with 403 }
Customers
Create a customer
$customer = $client->customers()->create([ 'merchant_customer_id' => 'user-42', // your internal user ID 'email' => 'fan@god.ba', 'name' => 'Adnan Fest', 'phone' => '+38761000000', ]); echo $customer->uuid; // Monri's UUID for this customer
Find a customer
// By Monri UUID $customer = $client->customers()->find('cust-uuid-123'); // By your own customer ID $customer = $client->customers()->findByMerchantId('user-42');
Update a customer
$customer = $client->customers()->update('cust-uuid-123', [ 'email' => 'new@god.ba', 'city' => 'Sarajevo', ]);
List customers
$customers = $client->customers()->list(limit: 50, offset: 0);
Delete a customer
$client->customers()->delete('cust-uuid-123');
Saved payment methods
$methods = $client->customers()->paymentMethods('cust-uuid-123'); foreach ($methods as $method) { echo $method->maskedPan; // '411111******1111' echo $method->expirationDate; // '12/27' echo $method->token; // pan_token for card-on-file payments echo $method->expired; // bool }
Card tokenization (frontend)
Generate a temporary token to pass to the Monri frontend SDK:
$token = $client->tokens()->generate(); // Pass these to your frontend echo $token->id; echo $token->timestamp; echo $token->digest;
Error handling
use Kemo\Monri\Exception\ApiException; use Kemo\Monri\Exception\MonriException; use Kemo\Monri\Exception\NetworkException; try { $payment = $client->payments()->create([...]); } catch (ApiException $e) { // HTTP 4xx/5xx from Monri echo $e->statusCode; // e.g. 422 $data = $e->decodedBody(); // parsed response array } catch (NetworkException $e) { // Connection failure, timeout } catch (MonriException $e) { // Configuration error }
Custom HTTP client
The default transport uses cURL. To use a PSR-18 client (e.g. Guzzle):
use Kemo\Monri\Http\PsrHttpClient; $http = new PsrHttpClient($guzzle, $requestFactory, $streamFactory); $client = new MonriClient($config, $http);
You can also implement HttpClientInterface directly:
interface HttpClientInterface { public function get(string $url, array $headers = []): array; public function post(string $url, string $body, array $headers = []): array; public function delete(string $url, array $headers = []): array; }
$body arrives already serialized and must be sent byte-for-byte. The
Monri request signature in the Authorization header is computed over exactly
those bytes, so re-encoding or reformatting the payload produces a signature
the gateway rejects.
Development
composer test # PHPUnit composer cs-check # php-cs-fixer (dry-run) composer cs-fix # php-cs-fixer (apply) composer phpcs # PHP_CodeSniffer composer phpcbf # PHP_CodeSniffer (auto-fix) composer phpstan # PHPStan (level max)
Environments
| Constant | Base URL |
|---|---|
Environment::Test |
https://ipgtest.monri.com |
Environment::Production |
https://ipg.monri.com |
License
MIT - see LICENSE.