mews/pos
A PHP SDK for the Mews POS API that helps Laravel and PHP applications connect to Mews point-of-sale services. Build hospitality and retail integrations with a clean client for orders, bills, and POS data.
Install
composer require mews/pos
Check composer.json for required PHP extensions before you start — most gateways need ext-curl, ext-dom, ext-mbstring, ext-json, plus ext-soap for the SOAP-based acquirers. Also grep the composer file for the php constraint; v2 targets modern PHP, v1 supports 7.x.
Where to look first (in this order)
docs/ — per-bank markdown files (garanti.md, akbank.md, isbank.md, …). Each documents the bank's quirks: required credential fields, 3D Secure flow type, supported installments, mandatory extra fields. Read the one for your bank before touching src/.examples/ — runnable end-to-end scripts for each gateway. These are the fastest way to see the exact object graph your version expects (factory names have changed between major versions).config/pos.php — the account/transaction configuration shape. Publish/copy it as your starting point rather than inventing keys.src/Gateways/ — one class per bank; that file is the source of truth when docs disagree with behavior.First use case: a test-mode non-secure payment
Create a gateway from the factory, build a credit-card value object, build a payment array, call pay():
use Mews\Pos\Factory\GatewayFactory;
use Mews\Pos\Factory\AccountFactory;
use Mews\Pos\Factory\CreditCardFactory;
$account = AccountFactory::create('garanti', [
'merchant_id' => env('POS_MERCHANT_ID'),
'username' => env('POS_USERNAME'),
'password' => env('POS_PASSWORD'),
'secret_key' => env('POS_SECRET_KEY'),
'test_mode' => true,
]);
$gateway = GatewayFactory::create('garanti', $account);
$card = CreditCardFactory::create('4824894728063019', '23', '07', '428', 'Test User');
$response = $gateway->pay([
'id' => 'order-1001',
'name' => 'Test User',
'amount' => 10.00,
'currency' => 'TRY',
'installment' => 0,
'ip' => request()->ip(),
'card' => $card,
]);
if ($response->isSuccess()) { /* ... */ }
Get a real test merchant ID from your bank (or from the examples/ file — some banks publish shared sandbox credentials). Run it, log the raw response, then do the same call with test_mode => false and a real 1-TRY transaction before building any UI on top of it.
Wrap the package in your own façade, don't sprinkle it through controllers. Bank gateways differ in status codes, error text and field names. Put a single PaymentGateway interface in your app (charge(), authorize(), capture(), refund(), cancel(), status()) and one MewsPosGateway adapter that satisfies it. Swapping acquirers, or adding a second one for failover, then costs one class instead of a grep across the codebase. Inject that interface, never PosInterface/gateway classes directly.
Separate configuration from code. Keep credentials per bank in a config/pos.php config file (or a payment_gateways DB table), and resolve which bank to use at runtime. This is also what lets you offer "choose your bank" in a checkout page — the gateway list, installments and BIN restrictions are all config-driven.
The 3D Secure flow is two requests, so model it as two. Pattern:
/checkout/pay → call the gateway's 3D method (e.g. get3DFormData() / make3DPayment() depending on version). You get back an HTML form and a gateway_url. Render the form and auto-submit it to the bank.payment3DResponse($request) / ...3DResponse() method, which validates the bank's hash and returns the final response.Persist the order in a pending state at step 1 and only mark it paid in step 2. Never trust a query-string status=success.
Distinguish 3D models. A few banks offer 3D_PAY/3D_HOST (store-side 3D, no redirect for some flows) alongside classic redirect 3D. Choose deliberately: redirect models make you deal with callbacks and timeouts; hosted-3D models shift liability but have stricter PCI/registration requirements. The docs/<bank>.md file states which are supported.
Pre-auth then capture for anything where you ship after payment (hotels, custom orders). Use the pre-auth transaction type, store the returned bank transaction ID, and post-auth/capture within the bank's window (often 7 days). Your DB needs: order_id, bank_order_id/transaction_id, auth_code, amount, currency, installment, status, raw_response.
Refund vs cancel is not symmetric. Cancel voids before settlement (same day, usually full amount only); refund works after settlement and can be partial. Most banks allow multiple partial refunds up to the total — track refunded amounts yourself and reconcile with status().
Reconcile on a schedule. Add a daily job that pulls status() / statement data for anything still pending or authorized. Banks drop callbacks, browsers close mid-3D, and timeouts produce ambiguous state. This job is what makes discrepancies visible instead of silently losing money.
Laravel wiring sketch:
// app/Providers/PaymentServiceProvider.php
$this->app->singleton(PaymentGateway::class, fn ($app) => new MewsPosGateway(
config('pos.gateway'),
config('pos.accounts'),
$app['log'],
));
Publish/copy the package config, put credentials in .env, and add the callback route to VerifyCsrfToken::$except.
Testing: two layers. Use the banks' sandbox environments and their published test cards for integration tests (mark these as @group external, they're slow and flaky). For unit tests, mock your own PaymentGateway interface — the package's response objects are simple enough but constructing them by hand is brittle across versions.
"10.00", some want minor units ("1000"), some want a comma decimal separator. Never pass floats; format to a string based on the bank's docs, and assert it in a test.pay() returns a response; check isSuccess() (or getStatus() === 'approved') and read getErrorCode() + getErrorMessage(). Wrap every call in a try/catch anyway — network/XML-parse exceptions do bubble up from the HTTP layer.$_POST yourself.VerifyCsrfToken::$except and no auth middleware. Also make sure your route isn't behind a proxy that rewrites the body.order_id, because a timeout doesn't mean the charge failed.merchant_id/terminal_id/prov_user/prov_password/store_key; İş Bankası and others use different names, and some need a client certificate file path. Copy the field names exactly from the bank's doc — unknown keys are often silently ignored, producing a cryptic "invalid merchant" error.PosFactory vs GatewayFactory, response method names, config array shape) are substantially different, and the README sometimes lags the code. Pin the version in composer.json ("mews/pos": "^2.0"), read CHANGELOG, and treat a major bump as a migration task.NOASSERTION; confirm the actual LICENSE contents before shipping commercially. In practice it's permissive, but verify rather than assume.How can I help you explore Laravel packages today?