PHPackages                             alrajhi/payment-gateway - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Payment Processing](/categories/payments)
4. /
5. alrajhi/payment-gateway

ActiveLibrary[Payment Processing](/categories/payments)

alrajhi/payment-gateway
=======================

Clean Laravel package for Al Rajhi Bank Payment Gateway - Bank Hosted Integration Only

v1.0.9(2mo ago)216↓81.8%1MITPHPPHP ^8.1

Since Mar 31Pushed 3w agoCompare

[ Source](https://github.com/YacoubAl-hardari/alrajhi-payment-gateway)[ Packagist](https://packagist.org/packages/alrajhi/payment-gateway)[ RSS](/packages/alrajhi-payment-gateway/feed)WikiDiscussions master Synced 4w ago

READMEChangelogDependencies (4)Versions (11)Used By (0)

[![alt text](1.png)](1.png)

AlRajhi Payment Gateway (Laravel Package)
=========================================

[](#alrajhi-payment-gateway-laravel-package)

Laravel package for integrating with Al Rajhi Bank payment gateway — compatible with **ARB Merchant Integration Guide REST v1.31**.

---

Features
--------

[](#features)

FeatureStatusBank Hosted — payment initiation✅Faster Checkout✅Iframe✅Apple Pay (Bank Hosted)✅Webhook — ARB notification handling✅Callback — `trandata` decryption✅BIN Check✅Payment status classification (CAPTURED / NOT CAPTURED / …)✅Inquiry API (`action=8`)✅Void / Refund / Capture❌ Coming soon---

Installation
------------

[](#installation)

```
composer require alrajhi/payment-gateway
php artisan vendor:publish --tag=alrajhi-config
```

---

Configuration (`.env`)
----------------------

[](#configuration-env)

```
ALRAJHI_BASE_URL=https://securepayments.alrajhibank.com.sa
ALRAJHI_ENVIRONMENT=sandbox
ALRAJHI_TRANPORTAL_ID=your_tranportal_id
ALRAJHI_TRANPORTAL_PASSWORD=your_password
ALRAJHI_RESOURCE_KEY=your_resource_key

ALRAJHI_URL_ENCODE_BEFORE_ENCRYPT=false
ALRAJHI_URL_DECODE_AFTER_DECRYPT=false
ALRAJHI_RETRY_RAW_TRANDATA_ON_INVALID=true

ALRAJHI_RESPONSE_URL=${APP_URL}/api/payment/success
ALRAJHI_ERROR_URL=${APP_URL}/api/payment/failed
ALRAJHI_WEBHOOK_SECRET=your_webhook_secret

ALRAJHI_STRICT_RESPONSE_MODE=false
ALRAJHI_ACCEPT_QUERY_RESPONSE=true
ALRAJHI_ACCEPT_DIRECT_CALLBACK_FIELDS=true

ALRAJHI_PREFER_CATALOG_MESSAGE=true
ALRAJHI_INCLUDE_OFFICIAL_MESSAGE=true
ALRAJHI_UDF_AUTO_FILL_DEFAULTS=false
ALRAJHI_CAPTURE_AUTO_SET_UDF7_R=true
```

> **Important (v1.31):** Every gateway request must include `X-FORWARDED-FOR` with the customer IP first. The package sets this automatically when calling `initiate()`.

---

Initiate Payment (Bank Hosted)
------------------------------

[](#initiate-payment-bank-hosted)

```
use AlRajhi\PaymentGateway\Facades\AlRajhiPayment;
use Illuminate\Http\Request;

Route::post('/test-payment', function (Request $request) {
    $trackId = 'TRK-' . now()->format('YmdHis') . '-' . random_int(1000, 9999);

    $forwardedFor = $request->header('X-Forwarded-For');
    $customerIp = is_string($forwardedFor) && $forwardedFor !== ''
        ? trim(explode(',', $forwardedFor)[0])
        : $request->ip();

    $payment = AlRajhiPayment::bankHosted()->initiate([
        'id' => config('alrajhi.credentials.tranportal_id'),
        'password' => config('alrajhi.credentials.tranportal_password'),
        'amount' => '1.00',
        'action' => '1',           // 1 = Purchase, 4 = Authorization
        'currencyCode' => '682',
        'trackId' => $trackId,
        'responseURL' => config('alrajhi.callbacks.response_url'),
        'errorURL' => config('alrajhi.callbacks.error_url'),
        'customerIp' => $customerIp,
    ]);

    return response()->json(['data' => $payment]);
});
```

**Successful response:**

```
{
  "success": true,
  "payment_id": "600202616049354939",
  "payment_url": "https://securepayments.alrajhibank.com.sa/pg/paymentpage.htm",
  "redirect_url": "https://securepayments.alrajhibank.com.sa/pg/paymentpage.htm?PaymentID=600202616049354939",
  "track_id": "TRK-20260609095147-9076"
}
```

Redirect the customer to `redirect_url` to complete payment.

---

Recommended Payment Flow
------------------------

[](#recommended-payment-flow)

```
1. initiate()          → Get redirect_url
2. Customer pays       → On ARB payment page
3. Callback            → UX only — do NOT finalize DB updates here
4. Webhook             → Update order in database (source of truth)
5. Inquiry (action=8)  → Server-side fallback (if Webhook is delayed or for reconciliation)

```

### When should payment status be updated?

[](#when-should-payment-status-be-updated)

StageWhen does it happen?Update DB?Role**Callback** (`success` / `failed`)When customer returns from ARB page❌ NoShow message to user only**Webhook**Usually before or after callback (async)✅ **Yes — here**Official source of truth**Inquiry**On demand from server (Job / Admin)✅ Yes (fallback)If Webhook is missing or for reconciliation> **Do not call Inquiry on every success/failed callback** — it slows the page and duplicates what Webhook already does.
> Professional pattern: Callback for UX → Webhook for DB update → Inquiry only on delay or reconciliation.

 ```
sequenceDiagram
    participant User as Customer
    participant ARB as ARB
    participant Callback as success/failed
    participant Webhook as /webhook
    participant DB as Database
    participant Inquiry as Inquiry API

    User->>ARB: Pays
    ARB->>Callback: Redirect (UX only)
    ARB->>Webhook: Async notification
    Webhook->>DB: Update status (paid/failed)
    Note over Inquiry,DB: Inquiry only if Webhook is delayedor via daily reconciliation job
    Inquiry->>ARB: action=8
    Inquiry->>DB: Fallback update
```

      Loading ---

Webhook (source of truth for order updates)
-------------------------------------------

[](#webhook-source-of-truth-for-order-updates)

The package registers automatically:

```
POST /alrajhi/webhook

```

**Example in your application:**

```
Route::post('/webhook', function (Request $request) {
    $rawBody = (string) $request->getContent();
    $decoded = json_decode($rawBody, true);
    $payload = is_array($decoded) ? $decoded : $request->all();

    $ack = AlRajhiPayment::webhook()->process(
        $payload,
        function (array $data, string $type): void {
            // Successful or pending payment
            if ($data['payment_outcome'] === 'success') {
                // Payment::where('track_id', $data['track_id'])
                //     ->update(['status' => 'paid', 'payment_id' => $data['payment_id']]);
            }

            if ($data['payment_outcome'] === 'pending') {
                // Payment::where('track_id', $data['track_id'])
                //     ->update(['status' => 'processing']);
            }
        },
        function (array $errorData, string $type): void {
            // Failed payment
            // Payment::where('track_id', $errorData['transaction_data']['track_id'] ?? null)
            //     ->update(['status' => 'failed']);
        }
    );

    // ARB requires this exact format
    return response()->json($ack); // [{"status":"1"}]
});
```

**Important fields in `$transactionData`:**

FieldDescription`payment_outcome``success` | `failure` | `pending` | `voided` | `cancelled``normalized_status``CAPTURED`, `NOT CAPTURED`, `APPROVED`, …`track_id`Your unique reference`payment_id`ARB payment ID`transaction_id`ARB `transId``amount`Transaction amount**ARB acknowledgment:**

```
[{"status": "1"}]
```

On processing failure:

```
[{"status": "0"}]
```

---

Callback (customer UX only)
---------------------------

[](#callback-customer-ux-only)

Do not rely on the callback to update order status — use it to display a message to the user.

```
Route::post('/api/payment/success', function (Request $request) {
    $result = AlRajhiPayment::bankHosted()->handleResponse($request->all());
    $paymentResult = AlRajhiPayment::bankHosted()->handleResponseData($result);

    return response()->json(array_merge($paymentResult, [
        'payment_status' => $paymentResult['status_final'] ?? 'unknown',
        'confirmation' => [
            'source' => 'callback',
            'authoritative' => false,
            'message' => 'UX only — update order via Webhook, not here',
            'await_webhook' => true,
        ],
    ]));
});

Route::post('/api/payment/failed', function (Request $request) {
    $result = AlRajhiPayment::bankHosted()->handleResponse($request->all());
    $paymentResult = AlRajhiPayment::bankHosted()->handleResponseData($result);

    return response()->json(array_merge($paymentResult, [
        'payment_status' => $paymentResult['status_final'] ?? 'failed',
        'confirmation' => [
            'source' => 'callback',
            'authoritative' => false,
            'message' => 'UX only — update order via Webhook, not here',
            'await_webhook' => true,
        ],
    ]));
});
```

### Real callback response examples

[](#real-callback-response-examples)

These are actual responses from `POST /api/payment/success` after processing with `handleResponse()` + `handleResponseData()`.

#### ✅ Successful payment (`CAPTURED`)

[](#-successful-payment-captured)

```
{
  "status_final": "success",
  "system_status": "success",
  "bank_status": "CAPTURED",
  "transId": 261601331202919,
  "date": "0609",
  "udf1": "",
  "udf2": "",
  "udf3": "",
  "udf4": "",
  "udf5": "",
  "authRespCode": "00",
  "authCode": "000000",
  "custid": null,
  "actionCode": "1",
  "ref": "616094017404",
  "result": "CAPTURED",
  "status": null,
  "is_success": true,
  "is_failure": false,
  "is_pending": false,
  "is_captured": true,
  "is_authorized": false,
  "is_cancelled": false,
  "is_voided": false,
  "error_code": null,
  "error_text": null,
  "payment_id": "600202616099491275",
  "track_id": "TRK-20260609103015-9910",
  "amount": "1.0",
  "card_type": "MASTERCARD",
  "card": "510510XXXXXX5100",
  "expMonth": "12",
  "expYear": "2027",
  "payment_status": "success"
}
```

> `actionCode: "1"` = purchase transaction type. `result: "CAPTURED"` = actual payment outcome.

#### ❌ Failed — 3DS not authenticated (`IPAY0100357`)

[](#-failed--3ds-not-authenticated-ipay0100357)

Customer did not complete card authentication (3DS). `bank_status` is `null` and `is_captured` is `false` even though `actionCode` is `"1"`.

```
{
  "status_final": "failed",
  "system_status": "failed",
  "bank_status": null,
  "transId": null,
  "date": null,
  "udf1": "",
  "udf2": "",
  "udf3": "",
  "udf4": "",
  "udf5": "",
  "authRespCode": null,
  "authCode": null,
  "custid": null,
  "actionCode": "1",
  "ref": null,
  "result": null,
  "status": null,
  "is_success": false,
  "is_failure": true,
  "is_pending": false,
  "is_captured": false,
  "is_authorized": false,
  "is_cancelled": false,
  "is_voided": false,
  "error_code": "IPAY0100357",
  "error_text": "!ERROR!-IPAY0100357-NOT AUTHENTICATED",
  "payment_id": "600202616099692365",
  "track_id": "TRK-20260609102333-4086",
  "amount": "1.0",
  "card_type": "VISA",
  "card": null,
  "expMonth": null,
  "expYear": null,
  "payment_status": "failed"
}
```

#### ❌ Failed — payment option not enabled (`IPAY0100260`)

[](#-failed--payment-option-not-enabled-ipay0100260)

Terminal configuration issue in the ARB merchant portal — enable Visa / Mastercard / MADA for the terminal.

```
{
  "status_final": "failed",
  "system_status": "failed",
  "bank_status": null,
  "transId": null,
  "date": null,
  "udf1": "",
  "udf2": "",
  "udf3": "",
  "udf4": "",
  "udf5": "",
  "authRespCode": null,
  "authCode": null,
  "custid": null,
  "actionCode": "1",
  "ref": null,
  "result": null,
  "status": null,
  "is_success": false,
  "is_failure": true,
  "is_pending": false,
  "is_captured": false,
  "is_authorized": false,
  "is_cancelled": false,
  "is_voided": false,
  "error_code": "IPAY0100260",
  "error_text": "!ERROR!-IPAY0100260-Payment option(s) not enabled",
  "payment_id": "600202616000417166",
  "track_id": "TRK-20260609102712-8561",
  "amount": "1.0",
  "card_type": null,
  "card": null,
  "expMonth": null,
  "expYear": null,
  "payment_status": "failed"
}
```

ResponseKey fields to checkMeaningSuccess`result: "CAPTURED"`, `is_captured: true`Payment completedAuth failed`error_code: "IPAY0100357"`, `bank_status: null`3DS authentication failedConfig error`error_code: "IPAY0100260"`, `bank_status: null`Enable card types in ARB portal> **Important:** Never use `actionCode` alone to determine payment success. Always check `result`, `bank_status`, and `is_captured`.

---

Payment Statuses (ARB v1.31)
----------------------------

[](#payment-statuses-arb-v131)

ARB `result`Meaning`payment_outcome``CAPTURED`Successful purchase`success``APPROVED`Successful authorization`success``NOT CAPTURED`Failed purchase`failure``NOT APPROVED`Failed authorization`failure``PROCESSING`Manual MADA refund in progress`pending``VOIDED`Void transaction`voided``DENIED BY RISK`Risk system rejection`failure``HOST TIMEOUT`Host timeout`pending`> **Note:** `actionCode` = transaction type (`1` purchase, `4` authorization) — **not** the payment result. Do not use it to determine whether a payment was successful.

---

Payment Verification (Inquiry API — `action=8`)
-----------------------------------------------

[](#payment-verification-inquiry-api--action8)

Use Inquiry for server-side verification when needed (e.g. if Webhook did not arrive). **Webhook remains the primary source** for order updates; Inquiry is for additional verification.

### By `transId` (most common)

[](#by-transid-most-common)

```
use AlRajhi\PaymentGateway\Facades\AlRajhiPayment;

$result = AlRajhiPayment::inquiry()->byTransId(
    transId: '261601253202722',
    amount: '1.00',
    trackId: 'TRK-20260609095147-9076',
);

// $result['status_final'] === 'success' && $result['result'] === 'CAPTURED' → paid
```

### By `PaymentID`

[](#by-paymentid)

```
$result = AlRajhiPayment::inquiry()->byPaymentId(
    paymentId: '600202616049354939',
    amount: '1.00',
    trackId: 'TRK-20260609095147-9076',
);
```

### By `trackId`

[](#by-trackid)

```
$result = AlRajhiPayment::inquiry()->byTrackId(
    trackId: 'TRK-20260609095147-9076',
    amount: '1.00',
);
```

### Generic inquiry

[](#generic-inquiry)

```
$result = AlRajhiPayment::inquiry()->inquire([
    'amount' => '1.00',
    'track_id' => 'TRK-20260609095147-9076',
    'reference_type' => 'TRANID',   // TRANID | PaymentID | TrackID
    'reference_id' => '261601253202722',
    'customer_ip' => '203.0.113.10', // optional — inferred from current request
]);
```

**Successful response:**

```
{
  "status_final": "success",
  "bank_status": "CAPTURED",
  "payment_status": "success",
  "is_success": true,
  "is_captured": true,
  "payment_id": "600202616049354939",
  "track_id": "TRK-20260609095147-9076",
  "transId": "261601253202722",
  "result": "CAPTURED",
  "amount": "1.00"
}
```

> **Notes:**
>
> - `track_id` must match the value used in `initiate()`.
> - `amount` must match the original transaction amount.
> - Inquiry does not require `responseURL` / `errorURL` — the package does not send them.
> - **Do not call Inquiry from success/failed pages** — use it in Jobs, Admin panels, or when Webhook is delayed.

### Test API route

[](#test-api-route)

```
POST /api/payment/inquiry
Content-Type: application/json

{
  "reference_type": "TRANID",
  "reference_id": "261601253202722",
  "amount": "1.00",
  "track_id": "TRK-20260609095147-9076"
}
```

**Response:**

```
{
  "success": true,
  "confirmation": {
    "source": "inquiry",
    "authoritative": true,
    "use_case": "reconciliation_or_webhook_fallback",
    "is_paid": true,
    "message": "Verified with bank — payment is successful"
  },
  "data": { "status_final": "success", "result": "CAPTURED" }
}
```

### Reconciliation job (example — if Webhook did not arrive within 5 minutes)

[](#reconciliation-job-example--if-webhook-did-not-arrive-within-5-minutes)

```
// app/Jobs/ReconcilePendingPayment.php
Payment::where('status', 'pending')
    ->where('created_at', '
