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

Abandoned → [yacoubalhaidari/alrajhi-payment-gateway](/?search=yacoubalhaidari%2Falrajhi-payment-gateway)Library[Payment Processing](/categories/payments)

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

Unofficial Laravel integration for Al Rajhi Bank Payment Gateway — Bank Hosted API only

v2.0.0(1w ago)235↓89.5%1MITPHPPHP ^8.1

Since Mar 31Pushed 1w 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 6d ago

READMEChangelogDependencies (8)Versions (15)Used By (0)

Unofficial Laravel Integration for Al Rajhi Bank Payment Gateway
================================================================

[](#unofficial-laravel-integration-for-al-rajhi-bank-payment-gateway)

Important

This is an independent, unofficial open-source Laravel package. It is not affiliated with, endorsed by, sponsored by, or maintained by Al Rajhi Bank. “Al Rajhi Bank” and its related trademarks belong to their respective owners. This package is intended only for merchants who have valid payment-gateway credentials issued through the bank’s official channels. Card details are entered on the bank’s hosted payment page — never on a page owned by this package.

> \[!تنبيه\] هذه مكتبة Laravel مستقلة وغير رسمية ومفتوحة المصدر. لا تتبع مصرف الراجحي، ولم يتم اعتمادها أو رعايتها أو تطويرها من قِبل المصرف. جميع الأسماء والعلامات التجارية المرتبطة بالمصرف مملوكة لأصحابها. المكتبة مخصصة للتجار الذين يمتلكون بيانات ربط صالحة صادرة عبر القنوات الرسمية للمصرف. إدخال بيانات البطاقة يتم في صفحة الدفع التابعة للمصرف، وليس داخل أي صفحة تابعة لهذه المكتبة.

Laravel package for integrating with Al Rajhi Bank payment gateway (ARB Merchant Integration Guide REST v1.31) — **Bank Hosted flow only**.

> **Migrating from `alrajhi/payment-gateway`?** Use the new package name below. After publishing the successor on Packagist, abandon the old package and point it to `yacoubalhaidari/alrajhi-payment-gateway`.

---

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 yacoubalhaidari/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)

The customer is redirected to the **bank’s hosted payment page** to enter card details. This package never collects card numbers, CVV, or passwords.

```
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]);
});
```

**Example successful response:**

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

Redirect the customer to `redirect_url` to complete payment on the bank’s page.

---

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

[](#recommended-payment-flow)

```
1. initiate()          → Get redirect_url
2. Customer pays       → On ARB payment page (card data stays with the bank)
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**Authoritative 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,
        ],
    ]));
});
```

### Example callback responses

[](#example-callback-responses)

Illustrative shapes returned after `handleResponse()` + `handleResponseData()`. IDs below are **examples only**.

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

[](#-successful-payment-captured)

```
{
  "status_final": "success",
  "system_status": "success",
  "bank_status": "CAPTURED",
  "transId": 100000000000001,
  "date": "0101",
  "udf1": "",
  "udf2": "",
  "udf3": "",
  "udf4": "",
  "udf5": "",
  "authRespCode": "00",
  "authCode": "000000",
  "custid": null,
  "actionCode": "1",
  "ref": "100000000001",
  "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": "600000000000000001",
  "track_id": "TRK-EXAMPLE-0001",
  "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": "600000000000000002",
  "track_id": "TRK-EXAMPLE-0002",
  "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": "600000000000000003",
  "track_id": "TRK-EXAMPLE-0003",
  "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: '100000000000001',
    amount: '1.00',
    trackId: 'TRK-EXAMPLE-0001',
);

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

### By `PaymentID`

[](#by-paymentid)

```
$result = AlRajhiPayment::inquiry()->byPaymentId(
    paymentId: '600000000000000001',
    amount: '1.00',
    trackId: 'TRK-EXAMPLE-0001',
);
```

### By `trackId`

[](#by-trackid)

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

### Generic inquiry

[](#generic-inquiry)

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

**Example successful response:**

```
{
  "status_final": "success",
  "bank_status": "CAPTURED",
  "payment_status": "success",
  "is_success": true,
  "is_captured": true,
  "payment_id": "600000000000000001",
  "track_id": "TRK-EXAMPLE-0001",
  "transId": "100000000000001",
  "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": "100000000000001",
  "amount": "1.00",
  "track_id": "TRK-EXAMPLE-0001"
}
```

**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', '
