storkplus / sylius-xpay-plugin
XPay (Egypt) payment gateway for Sylius: hosted checkout, signed webhooks, refunds.
Package info
github.com/Storkplus/SyliusXPayPlugin
Type:sylius-plugin
pkg:composer/storkplus/sylius-xpay-plugin
Requires
- php: ^8.2
- sylius/sylius: ^2.1
- symfony/http-client: ^6.4 || ^7.0
Requires (Dev)
- behat/behat: ^3.16
- dbrekelmans/bdi: ^1.4
- dmore/behat-chrome-extension: ^1.4
- dmore/chrome-mink-driver: ^2.9
- friends-of-behat/mink: ^1.11
- friends-of-behat/mink-browserkit-driver: ^1.6
- friends-of-behat/mink-debug-extension: ^2.1
- friends-of-behat/mink-extension: ^2.7
- friends-of-behat/page-object-extension: ^0.3
- friends-of-behat/suite-settings-extension: ^1.1
- friends-of-behat/symfony-extension: ^2.6
- friends-of-behat/variadic-extension: ^1.6
- nyholm/psr7: ^1.8
- phpstan/phpstan: ^1.12
- phpstan/phpstan-doctrine: ^1.3
- phpstan/phpstan-webmozart-assert: ^1.2
- phpunit/phpunit: ^10.5
- sylius-labs/coding-standard: ^4.4
- sylius-labs/suite-tags-extension: ~0.2
- sylius/sylius-rector: ^2.0
- sylius/test-application: ^2.0.0@alpha
- symfony/browser-kit: ^6.4 || ^7.4
- symfony/debug-bundle: ^6.4 || ^7.4
- symfony/dotenv: ^6.4 || ^7.4
- symfony/intl: ^6.4 || ^7.4
- symfony/runtime: ^6.4 || ^7.4
- symfony/web-profiler-bundle: ^6.4 || ^7.4
- symfony/webpack-encore-bundle: ^2.2
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is not auto-updated.
Last update: 2026-10-19 15:55:21 UTC
README
XPay payment gateway for Sylius 2.1+: cards (with 3-D Secure), Fawry and ValU, through XPay's Hosted Checkout. Built on the Sylius 2 Payment Request architecture (no Payum).
- Hosted Checkout Session per payment, redirect to
checkout.xpay.app, return to the shop. - Signed webhooks (
XPay-Signature, HMAC-SHA256, 5-minute replay window) are the source of truth for payment status. - Asynchronous methods (Fawry): the order is placed with the payment in
processing, completed when the reference is paid. - Refunds from the Sylius admin call
POST /refunds(idempotent). - Cancelled payments expire the open Checkout Session.
- Admin form in English and Arabic, with the webhook URL to paste into XPay.
Requirements
| Version | |
|---|---|
| PHP | 8.2+ |
| Sylius | 2.1, 2.2, 2.3 |
Installation
The package is private; add the repository and require it:
composer config repositories.sylius-xpay-plugin vcs https://github.com/Storkplus/SyliusXPayPlugin composer require storkplus/sylius-xpay-plugin
Register the bundle in config/bundles.php:
Storkplus\SyliusXPayPlugin\StorkplusSyliusXPayPlugin::class => ['all' => true],
Import its configuration in config/packages/storkplus_sylius_xpay.yaml:
imports: - { resource: "@StorkplusSyliusXPayPlugin/config/config.yaml" } # Optional, these are the defaults: # storkplus_sylius_xpay: # api_base_url: 'https://api.xpay.app' # timeout: 20
Import its route (the static webhook endpoint) in config/routes/storkplus_sylius_xpay.yaml:
storkplus_sylius_xpay: resource: "@StorkplusSyliusXPayPlugin/config/routes.yaml"
No database migrations are needed: payments go through the Sylius routes (/pay/{hash}, /after-pay/{hash})
and the plugin stores what it needs in the payment details.
Clear the cache. The payment request messenger transport must stay synchronous (the Sylius default,
SYLIUS_MESSENGER_TRANSPORT_PAYMENT_REQUEST_DSN=sync://) so the customer is redirected in the same request.
Setting up a payment method
- In XPay, open Developers → API keys and copy a secret key (
sk_test_…while testing). - In Sylius admin, Configuration → Payment methods → Create → XPay (cards, Fawry, ValU).
The form shows the shop's webhook URL,
https://<your-shop>/payment/xpay/webhook. It is fixed per shop (it does not depend on the method code), so it can be registered right away. - Fill in the code, channels, name and secret key.
- In XPay, open Developers → Webhooks → Add endpoint, paste the URL and subscribe to:
checkout.session.completedcheckout.session.async_payment_succeededcheckout.session.async_payment_failedcheckout.session.expired
- Copy the
whsec_…signing secret XPay shows once, paste it into Webhook signing secret, save.
When a shop has several XPay methods (e.g. test and live), each event is matched to the method whose signing
secret verifies it. The per-method Sylius endpoint /payment-methods/{code} keeps working as well.
Test and live mode are separate in XPay: switching to live means a sk_live_… key and a new live
webhook endpoint with its own whsec_… secret.
Optional settings:
- Checkout page language: follow the order locale (Arabic/English), or force one.
- Payment method configuration ID: restrict which methods XPay shows (Settings → Payment methods in XPay).
From the command line
# Check the key, print the webhook URL bin/console storkplus:xpay:webhook xpay --base-url=https://mystore.example # Also create the endpoint through the API and store its signing secret bin/console storkplus:xpay:webhook xpay --base-url=https://mystore.example --register
How a payment flows
Sylius checkout complete
└─ /pay/{hash} capture request (new)
└─ POST /checkout/sessions Idempotency-Key = payment request hash
└─ 303 → checkout.xpay.app/c/cs_…
customer pays (3-D Secure / Fawry reference / ValU)
├─ webhook POST /payment/xpay/webhook (signature verified first, then the Sylius notify flow)
│ checkout.session.completed / async_payment_* / expired → payment state
└─ redirect /pay/{hash}?xpay_return=success|cancel
└─ GET /checkout/sessions/{id} → payment state
└─ /after-pay/{hash} → thank-you page (paid or processing) or "pay again" (failed)
Payment states:
| XPay session | Sylius payment |
|---|---|
paymentStatus: paid (amount and currency match) |
completed |
status: complete, paymentStatus: unpaid (Fawry reference issued) |
processing |
| open, bank verification still running | processing |
checkout.session.async_payment_failed / expired |
failed (Sylius opens a new payment) |
| customer back via the cancel URL, nothing in flight | session expired, payment failed |
Safety rules:
- The return redirect is never trusted on its own: the session is always read from the API, and webhooks
are applied only after
XPay-Signatureverification. - A session whose amount or currency differs from the Sylius payment never completes it (logged as critical).
- Events for a session that is no longer bound to the payment, for sessions Sylius did not create
(Payment Links, another shop on the same account) and for other event types are acknowledged with
204and ignored, so they never cause retries or failed-delivery emails. - Invalid signatures get
400. - The webhook and the customer's return can arrive together; the order row is locked (
SELECT … FOR UPDATE) and re-read before any state change, so the second one is a no-op instead of a duplicate "paid" event.
Amounts: Sylius stores two implied decimals for every currency; for KWD, BHD, JOD, OMR and LYD the plugin converts to XPay's three-decimal minor unit.
Testing with XPay test mode
Use card 5123 4500 0000 0008, expiry 01/39, CVV 100 for an approved payment. Other expiries simulate
declines (05/39), insufficient funds (01/27) and more; see XPay test mode.
Fawry and ValU need a total of at least 50 EGP.
Development
composer install vendor/bin/phpunit --testsuite unit vendor/bin/phpstan analyse -c phpstan.neon vendor/bin/ecs check APP_ENV=test vendor/bin/console lint:container # End to end (Docker): test application + MySQL + a mock XPay API sending signed webhooks. # Covers card payment, decline and retry, Fawry, forged webhook, refund and the admin form. tests/E2E/run.sh
Logs go to the xpay Monolog channel.