Yii2 eCitizen Payment Extension ¶
- What This Package Does
- What This Package Does Not Do
- Compatibility
- Installation
- Yii2 Configuration
- Environment Variables
- Yii2 MVC Pattern
- Basic Checkout Usage
- Callback URL Building
- Example Model: Payment Form
- Example Controller: Checkout
- Example View: Checkout Iframe
- Optional Built-In Notification Controller
- Example Callback Action In Your App
- Notification Data
- Query Payment Status
- Localhost and Public Callback URLs
- Security Notes
- Suggested Database Fields
- Publishing To Packagist
- Adding To Yii Framework Extensions
- Troubleshooting
- Development Checks
Yii2 Composer extension for eCitizen iframe payments.
This package wraps the reusable eCitizen payment gateway parts in Yii2 components so you can call them from normal Yii2 MVC code: models/forms validate user input, controllers call Yii::$app->payments->gateway('ecitizen'), and callback actions receive eCitizen asynchronous notifications.
It is intentionally not tied to a particular housing, school, fee, banking-slip, or invoice table. Your Yii application owns the order records, customer records, payment records, and reconciliation workflow.
What This Package Does ¶
- Builds eCitizen iframe checkout payloads
- Generates the eCitizen
secureHash - Validates eCitizen notification signatures
- Extracts normalized notification data
- Queries eCitizen payment status by invoice/reference
- Builds callback URLs from the hosting Yii application
- Provides an optional notification controller
- Provides a small iframe helper view
What This Package Does Not Do ¶
- It does not create your application's invoices or orders.
- It does not update your housing, student, rent, fee, or banking tables.
- It does not decide whether a payment should be accepted.
- It does not store callbacks automatically.
- It does not reconcile paid invoices into your own accounting records.
The expected pattern is: this extension handles eCitizen communication; your application handles business logic and persistence.
Compatibility ¶
- PHP
8.0+ - Yii2
2.0.45+ - Composer package name:
josemodi97/yii2-ecitizen-payment - Yii namespace:
josemodi97\ecitizen
Installation ¶
All paths in this guide are relative to the root of the Yii2 application that will use this package. For a basic Yii2 app, that is the folder that contains composer.json, config/, controllers/, models/, and views/.
After publishing to Packagist:
composer require josemodi97/yii2-ecitizen-payment
If the package is kept in a local folder, edit the consuming Yii2 app's composer.json:
Path from Yii2 app root: composer.json
{
"repositories": [
{
"type": "path",
"url": "../yii2-ecitizen-payment"
}
],
"require": {
"josemodi97/yii2-ecitizen-payment": "*"
}
}
Then run:
composer update josemodi97/yii2-ecitizen-payment
Yii2 Configuration ¶
Add the payment component to the Yii2 application config.
Basic Yii2 Template ¶
Add the component in:
- App config:
config/web.php
Advanced Yii2 Template ¶
Add the component in each application config that needs eCitizen access:
- Frontend:
frontend/config/main.php - Backend:
backend/config/main.php - Console:
console/config/main.php
'components' => [
'payments' => [
'class' => josemodi97\ecitizen\Payment::class,
'gateways' => [
'ecitizen' => [
'class' => josemodi97\ecitizen\gateways\EcitizenGateway::class,
'apiClientID' => getenv('ECITIZEN_API_CLIENT_ID'),
'apiKey' => getenv('ECITIZEN_API_KEY'),
'secret' => getenv('ECITIZEN_SECRET'),
'serviceID' => getenv('ECITIZEN_SERVICE_ID'),
'callbackBaseUrl' => getenv('ECITIZEN_CALLBACK_BASE_URL') ?: null,
],
],
],
],
callbackBaseUrl is optional in normal web requests. If it is not set, the gateway tries to derive the base URL from the current Yii request, for example https://housing.example.com. Set it explicitly for console jobs, queue workers, reverse-proxy deployments, or local development through a public HTTPS tunnel.
Full Gateway Options ¶
'ecitizen' => [
'class' => josemodi97\ecitizen\gateways\EcitizenGateway::class,
'apiClientID' => getenv('ECITIZEN_API_CLIENT_ID'),
'apiKey' => getenv('ECITIZEN_API_KEY'),
'secret' => getenv('ECITIZEN_SECRET'),
'serviceID' => getenv('ECITIZEN_SERVICE_ID'),
'currency' => 'KES',
'url' => 'https://payments.ecitizen.go.ke/PaymentAPI/iframev2.1.php',
'statusUrl' => 'https://payments.ecitizen.go.ke/api/invoice/payment/status',
'callbackBaseUrl' => getenv('ECITIZEN_CALLBACK_BASE_URL') ?: null,
'allowedGatewayHosts' => ['payments.ecitizen.go.ke'],
'pictureURL' => '',
'sendSTK' => false,
'caBundlePath' => null,
'timeout' => 30,
],
Environment Variables ¶
Create a .env file in the Yii2 application root if your app uses dotenv-style environment loading.
Path from Yii2 basic app root: .env
Path from Yii2 advanced project root: .env
ECITIZEN_API_CLIENT_ID=your_api_client_id
ECITIZEN_API_KEY=your_api_key
ECITIZEN_SECRET=your_secret
ECITIZEN_SERVICE_ID=your_service_id
ECITIZEN_CALLBACK_BASE_URL=https://your-domain.example
Yii2 does not load .env files by default in every template. If your application already loads .env, getenv('ECITIZEN_API_CLIENT_ID') will work as shown above. If it does not, install and bootstrap a dotenv loader in the Yii2 application, or set these variables in your server environment.
If you choose the dotenv approach, install the loader in the Yii2 application:
composer require vlucas/phpdotenv
Example using vlucas/phpdotenv in a Yii2 basic app:
Path from Yii2 app root: web/index.php
require __DIR__ . '/../vendor/autoload.php';
if (class_exists('Dotenv\\Dotenv')) {
$dotenv = Dotenv\Dotenv::createImmutable(dirname(__DIR__));
$dotenv->safeLoad();
}
require __DIR__ . '/../vendor/yiisoft/yii2/Yii.php';
Example using vlucas/phpdotenv in a Yii2 advanced app:
Common entry files from project root:
- Frontend:
frontend/web/index.php - Backend:
backend/web/index.php - Console:
yii
Load .env before requiring common/config/bootstrap.php or before reading config files:
require __DIR__ . '/../../vendor/autoload.php';
if (class_exists('Dotenv\\Dotenv')) {
$dotenv = Dotenv\Dotenv::createImmutable(dirname(__DIR__, 2));
$dotenv->safeLoad();
}
Do not hard-code real eCitizen keys, secrets, service IDs, or callback URLs in code. Move all secrets to environment variables.
Yii2 MVC Pattern ¶
A clean Yii2 integration usually looks like this:
- Model or form: validates amount, invoice reference, customer name, phone, and email.
- Controller: receives the user request, creates a local pending payment record, builds the eCitizen payload, and renders the checkout.
- Callback controller action: receives eCitizen notifications and stores the raw payload.
- Service or ActiveRecord layer: marks the local order paid only after signature validation and status checks.
- Reconciliation job: rechecks pending or suspicious payments using
queryPaymentStatus().
Basic Checkout Usage ¶
Place these calls inside your own controller action, service class, console command, or model method.
$gateway = Yii::$app->payments->gateway('ecitizen');
$payload = $gateway->createCheckoutPayload([
'amount' => 1000,
'reference' => 'INV-1001',
'description' => 'Application fee',
'clientName' => 'Jane Doe',
'clientEmail' => 'jane@example.com',
'clientPhone' => '0712345678',
]);
$gatewayUrl = $gateway->gatewayUrl();
The payload includes eCitizen fields such as:
apiClientID
amountExpected
serviceID
billRefNumber
billDesc
currency
clientMSISDN
clientName
clientEmail
callBackURLOnSuccess
notificationURL
secureHash
Post the returned payload to $gatewayUrl using a normal form or an iframe.
Callback URL Building ¶
The gateway can build callback URLs from the hosting Yii application:
$notificationUrl = Yii::$app->payments
->gateway('ecitizen')
->buildCallbackUrl('/ecitizen-payment/ecitizen-notify');
You may override the base URL for one call:
$notificationUrl = Yii::$app->payments
->gateway('ecitizen')
->buildCallbackUrl('/ecitizen-payment/ecitizen-notify', 'https://tunnel.example.com');
If you do not pass notificationURL or callBackURLOnSuccess to createCheckoutPayload(), the gateway will build defaults:
'notificationURL' => '/ecitizen-payment/ecitizen-notify'
'callBackURLOnSuccess' => '/payment/success'
The final URLs are absolute URLs based on the host Yii application.
Example Model: Payment Form ¶
Path from Yii2 app root: models/EcitizenPaymentForm.php
<?php
namespace app\models;
use yii\base\Model;
class EcitizenPaymentForm extends Model
{
public $amount;
public $reference;
public $description;
public $clientName;
public $clientEmail;
public $clientPhone;
public function rules()
{
return [
[['amount', 'reference', 'description', 'clientName'], 'required'],
['amount', 'number', 'min' => 1],
[['reference', 'description', 'clientName', 'clientEmail', 'clientPhone'], 'trim'],
['reference', 'string', 'max' => 50],
['description', 'string', 'max' => 100],
['clientEmail', 'email'],
['clientPhone', 'string', 'max' => 20],
];
}
}
Example Controller: Checkout ¶
Path from Yii2 app root: controllers/EcitizenController.php
<?php
namespace app\controllers;
use app\models\EcitizenPaymentForm;
use Yii;
use yii\web\Controller;
use yii\web\Response;
class EcitizenController extends Controller
{
public function actionCreate()
{
$model = new EcitizenPaymentForm();
if ($model->load(Yii::$app->request->post()) && $model->validate()) {
/*
* Save your local pending payment record before redirecting/rendering
* checkout. The extension does not do database writes for you.
*/
$gateway = Yii::$app->payments->gateway('ecitizen');
$payload = $gateway->createCheckoutPayload([
'amount' => $model->amount,
'reference' => $model->reference,
'description' => $model->description,
'clientName' => $model->clientName,
'clientEmail' => $model->clientEmail,
'clientPhone' => $model->clientPhone,
'notificationURL' => $gateway->buildCallbackUrl('/ecitizen-payment/ecitizen-notify'),
'callBackURLOnSuccess' => $gateway->buildCallbackUrl('/ecitizen/success'),
]);
return $this->render('checkout', [
'gatewayUrl' => $gateway->gatewayUrl(),
'payload' => $payload,
]);
}
return $this->render('create', [
'model' => $model,
]);
}
public function actionSuccess()
{
return $this->render('success');
}
}
Example View: Checkout Iframe ¶
Path from Yii2 app root: views/ecitizen/checkout.php
<?php
use yii\helpers\Html;
/** @var string $gatewayUrl */
/** @var array $payload */
$this->title = 'Continue to eCitizen';
?>
<h1><?= Html::encode($this->title) ?></h1>
<form id="ecitizen-checkout-form" method="post" action="<?= Html::encode($gatewayUrl) ?>" target="ecitizen-checkout-frame" autocomplete="off">
<?php foreach ($payload as $name => $value): ?>
<?= Html::hiddenInput((string) $name, (string) $value) ?>
<?php endforeach; ?>
</form>
<iframe name="ecitizen-checkout-frame" title="eCitizen checkout" style="width:100%;min-height:640px;border:0"></iframe>
<?php
$this->registerJs("document.getElementById('ecitizen-checkout-form').submit();");
The package also includes a small reusable partial:
echo $this->render('@vendor/josemodi97/yii2-ecitizen-payment/views/payment/_iframe', [
'gatewayUrl' => $gatewayUrl,
'payload' => $payload,
'frameName' => 'ecitizen-checkout-frame',
]);
Optional Built-In Notification Controller ¶
The package includes a lightweight controller for receiving eCitizen notifications.
Add it to your app config:
'controllerMap' => [
'ecitizen-payment' => josemodi97\ecitizen\controllers\PaymentController::class,
],
Notification endpoint:
/ecitizen-payment/ecitizen-notify
The built-in action:
- reads POST data
- falls back to raw JSON body
- validates the eCitizen secure hash
- extracts normalized notification data
- returns JSON
It does not save to your database. For production systems, create your own callback action or extend the controller so you can store the raw payload and update your payment records.
Example Callback Action In Your App ¶
Path from Yii2 app root: controllers/EcitizenCallbackController.php
<?php
namespace app\controllers;
use Yii;
use yii\web\Controller;
use yii\web\Response;
class EcitizenCallbackController extends Controller
{
public $enableCsrfValidation = false;
public function actionNotify()
{
Yii::$app->response->format = Response::FORMAT_JSON;
$payload = Yii::$app->request->post();
if ($payload === []) {
$decoded = json_decode(Yii::$app->request->rawBody, true);
$payload = is_array($decoded) ? $decoded : [];
}
$gateway = Yii::$app->payments->gateway('ecitizen');
if (!$gateway->validateNotificationHash($payload)) {
Yii::$app->response->statusCode = 400;
return ['success' => false, 'message' => 'Invalid eCitizen notification signature.'];
}
$notification = $gateway->extractNotification($payload);
/*
* Store the raw payload first.
* Then find your local payment using $notification['reference'].
* Mark it paid only if the status and amount are acceptable.
*/
return [
'success' => true,
'reference' => $notification['reference'],
];
}
}
Notification Data ¶
extractNotification() returns a normalized array:
[
'provider' => 'ecitizen',
'reference' => 'INV-1001',
'gatewayReference' => 'ECITIZEN-INVOICE-NO',
'amount' => 1000.00,
'paymentDate' => '2026-07-18 12:30:00',
'status' => 'PAID',
'raw' => [],
]
Use:
josemodi97\ecitizen\gateways\EcitizenGateway::notificationStatusIsPaid($notification['status'])
to check common settled statuses:
PAID
SUCCESS
COMPLETED
SETTLED
Query Payment Status ¶
Use status queries for reconciliation, manual refresh buttons, cron jobs, or when a callback was missed.
$status = Yii::$app->payments
->gateway('ecitizen')
->queryPaymentStatus('INV-1001');
The extension signs the status query using:
api_client_id
client_invoice_ref
secure_hash
Your application should inspect the response and decide whether the local payment can be marked as paid.
Localhost and Public Callback URLs ¶
Callback actions belong in a web controller because eCitizen calls them over HTTPS.
localhost cannot receive eCitizen callbacks directly because it is only reachable from your own machine. For local development, expose the Yii app through a secure public HTTPS tunnel, then set:
ECITIZEN_CALLBACK_BASE_URL=https://your-tunnel-url.example
For a deployed housing application, you can omit callbackBaseUrl when Yii sees the correct public host. If the app runs behind a proxy or a queue/console command builds the payload, configure it explicitly:
ECITIZEN_CALLBACK_BASE_URL=https://your-public-url.example
Always store the raw callback payload before transforming it. This makes reconciliation much easier when eCitizen sends unexpected fields.
Security Notes ¶
- Keep callback actions CSRF-exempt because eCitizen will not send a Yii CSRF token.
- Validate the eCitizen secure hash before trusting a callback.
- Do not rely only on the user returning to the success URL.
- Treat the notification URL as the source of asynchronous payment updates.
- Re-query eCitizen before force-marking old pending payments as paid.
- Keep credentials out of Git.
- Use HTTPS for all callback URLs.
- Keep
allowedGatewayHostsrestricted to eCitizen hosts. - Log failed validation attempts without logging secrets.
Suggested Database Fields ¶
Your host application can use any schema, but these fields are useful:
id
reference
amount
currency
description
customer_name
customer_email
customer_phone
gateway
gateway_reference
status
raw_checkout_payload
raw_callback_payload
created_at
paid_at
last_status_checked_at
Suggested statuses:
pending
paid
failed
cancelled
expired
review_required
Publishing To Packagist ¶
Recommended package name:
josemodi97/yii2-ecitizen-payment
Recommended Yii extension title:
Yii2 eCitizen Payment
Recommended short description:
Yii2 component for eCitizen iframe checkout, notification validation, and payment status queries.
Submit this public repository URL to Packagist:
https://github.com/JoseModi97/yii2-ecitizen-payment
Adding To Yii Framework Extensions ¶
Yii2 extensions are Composer packages. To appear correctly as a Yii extension, the package should:
- be hosted in a public VCS repository such as GitHub
- contain a valid
composer.jsonat the repository root - use a Yii-style package name such as
josemodi97/yii2-ecitizen-payment - set Composer package type to
yii2-extension - require
yiisoft/yii2 - be registered on Packagist
This package already uses:
{
"name": "josemodi97/yii2-ecitizen-payment",
"type": "yii2-extension",
"require": {
"yiisoft/yii2": "^2.0.45"
}
}
After the GitHub repository is pushed and Packagist has imported it, open Yii Framework Extensions:
https://www.yiiframework.com/extensions
Search for:
josemodi97/yii2-ecitizen-payment
If it does not appear immediately, wait for Packagist/Yii indexing to refresh. Yii extension discovery depends on public Composer metadata, so the Packagist package must exist first.
Recommended Yii extension title:
Yii2 eCitizen Payment
Recommended Yii extension description:
Yii2 component for eCitizen iframe checkout, notification validation, callback URL generation, and payment status queries.
Troubleshooting ¶
callbackBaseUrl is not configured ¶
This means the gateway could not derive a URL from the current Yii request. Set:
ECITIZEN_CALLBACK_BASE_URL=https://your-public-url.example
eCitizen rejects the checkout payload ¶
Check:
apiClientIDapiKeysecretserviceIDamountcurrencybillRefNumbersecureHash
Also confirm that the service ID belongs to the configured eCitizen client account.
Notification validation fails ¶
Check that the eCitizen account sending the callback uses the same apiKey and secret configured in the Yii app. Store the raw payload temporarily during testing so you can compare the received fields.
Local callbacks do not arrive ¶
Use a public HTTPS tunnel and configure:
ECITIZEN_CALLBACK_BASE_URL=https://your-tunnel-url.example
Do not use plain http://localhost for real gateway callbacks.
Development Checks ¶
Run PHP syntax checks:
php -l src/gateways/EcitizenGateway.php
Validate Composer metadata:
composer validate --no-check-publish
If you have any questions, please ask in the forum instead.
Signup or Login in order to comment.