> ## Documentation Index
> Fetch the complete documentation index at: https://docs.avraapi.com/llms.txt
> Use this file to discover all available pages before exploring further.

# UPG Quick Setup

> Connect a website to the Universal Payment Gateway with the PHP SDK and optional Payment Elements.

This guide is the fastest complete way to add Universal Payment Gateway (UPG) checkout to a website. It uses one server-side SDK flow for every payment gateway your project is allowed to use, while [Payment Elements](/universal-payment-gateway/payment-elements/overview) provides the optional browser form and method selector.

You do **not** build separate checkout logic for PayHere, OnePay, DirectPay, Stripe, or another configured gateway. Your server asks AvraAPI what is available, creates one payment session for the buyer’s selected available method, and completes the payment only after verified provider evidence arrives.

<Warning>
  Keep the SDK, Client ID, Client Secret, gateway environment, `completionContext`, and payment completion logic on your backend. Payment Elements receives only browser-safe availability data and public checkout instructions.
</Warning>

## What you will build

| Part | Responsibility |
| - | - |
| Checkout page | Renders Payment Elements and sends a selected method plus customer details to your server. |
| Your backend checkout endpoint | Validates the selected method against `availability()`, creates a pending order, calls `createOrder()`, and returns only public session data. |
| Shared payment-completion handler | Receives provider callbacks and returns, loads the saved pending order, calls `completePayment()`, and fulfils the order only after a verified success. |

`/payments/checkout-session` used in this guide is an **example URL in your own application**. It is not an AvraAPI endpoint, SDK route, or required file name. Use the route structure that fits your application.

## Before you start

1. Create an AvraAPI project and keep its Client ID and Client Secret on your server.
2. Connect the project to an active Workspace UPG slot.
3. Configure at least one gateway profile in the Gateway Vault and make sure the intended domain is allowed.
4. Create one shared payment-completion handler and expose its public HTTPS callback and return routes.
5. Create a database record or equivalent durable store for your own pending orders. It must retain the gateway, amount, currency, and server-only `completionContext`.

### One completion handler, not one file per gateway

UPG has one universal server-side completion operation: `completePayment()`. You can keep the integration to two application components: a checkout-session handler and a shared payment-completion handler.

Payment providers do not send the same HTTP payload. For example, Stripe sends a signed raw webhook body, DirectPay sends a raw body plus an HMAC authorization header, and PayHere posts form fields. Use one of the following server-controlled routing styles. In both cases, every URL reaches the **same** completion-handler file or controller.

<Tabs>
  <Tab title="Routes — recommended">
    ```text theme={null}
    POST https://shop.example.com/payments/callback/payhere   ┐
    POST https://shop.example.com/payments/callback/stripe    │
    POST https://shop.example.com/payments/callback/directpay │
    GET  https://shop.example.com/payments/return/marxpay     ├─ one shared completion handler
    GET  https://shop.example.com/payments/return/webxpay     │
    GET  https://shop.example.com/payments/return/onepay      ┘
    ```

    Use this style when your framework supports named routes. The `{gateway}` route value is selected by the server route and must be checked against your enabled gateway allow-list before it reaches the handler.
  </Tab>

  <Tab title="Parameters — simple PHP">
    ```text theme={null}
    POST https://shop.example.com/payment-completion.php?gateway=payhere&source=webhook
    POST https://shop.example.com/payment-completion.php?gateway=stripe&source=webhook
    GET  https://shop.example.com/payment-completion.php?gateway=webxpay&source=return
    GET  https://shop.example.com/payment-completion.php?gateway=onepay&source=return
    ```

    This is still one physical `payment-completion.php` file. Read `gateway` and `source` only from `$_GET` and validate both against your server-side allow-list. Do not use `$_REQUEST`, form data, JSON, or a buyer-supplied value to select a gateway.
  </Tab>
</Tabs>

The route or parameter is configured by your server or in the provider dashboard; it is not selected by the buyer. It is a dispatch hint, not a secret or payment proof. The provider payload wrapper and the stored signed `completionContext` still perform the real verification.

<Note>
  Do not use one identical URL for every provider unless your handler has a deterministic, reviewed gateway-dispatch design. The PHP SDK does not provide an automatic all-gateway payload detector.
</Note>

## The quick flow

<Steps>
  <Step title="Check availability">
    Your backend calls `availability()`.
  </Step>

  <Step title="Display methods">
    Payment Elements displays only the returned available methods.
  </Step>

  <Step title="Customer input">
    The buyer selects a method and enters customer details.
  </Step>

  <Step title="Create session">
    Your backend checkout endpoint validates the method and calls `createOrder()`.
  </Step>

  <Step title="Launch checkout">
    The browser launches the returned provider checkout.
  </Step>

  <Step title="Receive callback">
    The provider callback or trusted return reaches your shared completion handler.
  </Step>

  <Step title="Verify and fulfil">
    Your backend calls `completePayment()`, verifies success, and fulfils the order once.
  </Step>
</Steps>

## The three UPG SDK calls

Every UPG checkout follows the same server-side sequence across the PHP, Laravel, and Node.js SDKs. Laravel 1.2.0 exposes the verified payment service through `AvraAPI::payment()`; Node.js SDK 1.2.0 exposes it through `client.payment()`. The full implementation guide below shows how to combine these calls with your own checkout page, order storage, and fulfilment code.

### 1. Discover available methods with `availability()`

Call `availability()` on your backend before showing payment choices. It returns only methods that the project, active UPG slot, configured gateway profiles, environment, and merchant domain currently allow.

**AvraAPI SDK call — real method**

<Tabs>
  <Tab title="PHP">
    ```php theme={null}
    $availability = $apix->payment()->availability('shop.example.com');

    if (! $availability->isReady()) {
        throw new RuntimeException($availability->message ?? 'Payments are unavailable.');
    }

    foreach ($availability->methods() as $method) {
        // $method['gateway'], $method['mode'], $method['available']
    }
    ```
  </Tab>

  <Tab title="Laravel">
    ```php theme={null}
    use Avraapi\Laravel\Facades\AvraAPI;

    $availability = AvraAPI::payment()->availability('shop.example.com');

    if (! $availability->isReady()) {
        throw new RuntimeException($availability->message ?? 'Payments are unavailable.');
    }

    foreach ($availability->methods() as $method) {
        // $method['gateway'], $method['mode'], $method['available']
    }
    ```
  </Tab>

  <Tab title="Node.js">
    ```ts theme={null}
    const availability = await client.payment().availability('shop.example.com');

    if (!availability.isReady()) {
      throw new Error(availability.message ?? 'Payments are unavailable.');
    }

    for (const method of availability.methods) {
      // method.gateway, method.mode, method.available
    }
    ```
  </Tab>
</Tabs>

Never trust a gateway or mode merely because the browser posts it. Validate the selected pair against a fresh `availability()` result again when creating the order.

### 2. Prepare checkout with `createOrder()`

Call `createOrder()` only after your server has calculated the order amount, currency, item description, URLs, and selected available gateway/mode. It returns a short-lived `PaymentSession` with public checkout instructions and a server-only `completionContext`.

**AvraAPI SDK call — real method**

<Tabs>
  <Tab title="PHP">
    ```php theme={null}
    $session = $apix->payment()->createOrder(new CreateOrderOptions(
        gateway: GatewayCode::PayHere,
        mode: CheckoutMode::Redirect,
        orderId: 'ORDER-2026-000184',
        items: 'Premium membership',
        amount: '1500.00',
        currency: 'LKR',
        customer: $customer,
        urls: $urls,
    ));

    // Browser-safe: $session->checkout, $session->gateway, $session->mode.
    // Server-only: $session->completionContext.
    ```
  </Tab>

  <Tab title="Laravel">
    ```php theme={null}
    use Avraapi\Laravel\Facades\AvraAPI;

    $session = AvraAPI::payment()->createOrder(new CreateOrderOptions(
        gateway: GatewayCode::PayHere,
        mode: CheckoutMode::Redirect,
        orderId: 'ORDER-2026-000184',
        items: 'Premium membership',
        amount: '1500.00',
        currency: 'LKR',
        customer: $customer,
        urls: $urls,
    ));

    // Browser-safe: $session->checkout, $session->gateway, $session->mode.
    // Server-only: $session->completionContext.
    ```
  </Tab>

  <Tab title="Node.js">
    ```ts theme={null}
    import { CheckoutMode, CreateOrderOptions, GatewayCode } from '@avraapi/node-sdk';

    const session = await client.payment().createOrder(new CreateOrderOptions({
      gateway: GatewayCode.PayHere,
      mode: CheckoutMode.Redirect,
      orderId: 'ORDER-2026-000184',
      items: 'Premium membership', amount: '1500.00', currency: 'LKR',
      customer, urls, merchantDomain: 'shop.example.com',
    }));

    // Browser-safe: session.checkout, session.gateway, session.mode.
    // Server-only: session.completionContext.
    ```
  </Tab>
</Tabs>

### 3. Verify and finish with `completePayment()`

Call `completePayment()` only from your backend after a provider callback or supported return reaches your shared completion handler. It returns the safe, normalized `PaymentCompletionResult` in `short()` mode by default. Passing `PaymentResponseOptions::short()` explicitly is optional; it can make the intended production response mode clearer in your code.

**AvraAPI SDK call — real method**

<Tabs>
  <Tab title="PHP">
    ```php theme={null}
    $result = $apix->payment()->completePayment(new PaymentCompletionOptions(
        gateway: GatewayCode::PayHere,
        completionContext: $pendingOrder->payment_completion_context,
        payload: $providerPayload,
        // Optional: omitting this uses the default short() response mode.
        response: PaymentResponseOptions::short(),
    ));

    // A verified result can still be failed, cancelled, pending, or unknown.
    // Fulfil only the verified succeeded state.
    ```
  </Tab>

  <Tab title="Laravel">
    ```php theme={null}
    use Avraapi\Laravel\Facades\AvraAPI;

    $result = AvraAPI::payment()->completePayment(new PaymentCompletionOptions(
        gateway: GatewayCode::PayHere,
        completionContext: $pendingOrder->payment_completion_context,
        payload: $providerPayload,
        // Optional: omitting this uses the default short() response mode.
        response: PaymentResponseOptions::short(),
    ));

    // A verified result can still be failed, cancelled, pending, or unknown.
    // Fulfil only the verified succeeded state.
    ```
  </Tab>

  <Tab title="Node.js">
    ```ts theme={null}
    import {
      GatewayCode,
      PaymentCompletionOptions,
      PaymentResponseOptions,
    } from '@avraapi/node-sdk';

    const result = await client.payment().completePayment(new PaymentCompletionOptions({
      gateway: GatewayCode.PayHere,
      completionContext: pendingOrder.completionContext,
      payload: providerPayload,
      response: PaymentResponseOptions.short(),
    }));
    ```
  </Tab>
</Tabs>

**The typed SDK result — real properties**

```php theme={null}
$safeCompletion = [
    'gateway' => $result->gateway->value,
    'verified' => $result->verified,
    'payment_status' => $result->paymentStatus->value,
    'provider_status' => $result->providerStatus,
    'order_id' => $result->orderId,
    'gateway_reference' => $result->gatewayReference,
    'amount' => $result->amount,
    'currency' => $result->currency,
    'request_id' => $result->requestId,
    'reconciliation' => $result->reconciliation,
];
```

The SDK maps the API's `data` object into this `PaymentCompletionResult`. There is no `$result->data` property. For the raw API envelope and the `short()`, `full()`, and `include()` response contracts, see [Payment Response & Completion](/universal-payment-gateway/advanced-setup/webhooks-and-completion).

### Handle every payment outcome

`verified` means that the provider evidence and signed completion context were accepted. It does **not** mean the payment succeeded. A signed callback can validly report a failed, cancelled, pending, or unknown payment.

| `paymentStatus` | Meaning | Merchant action |
| - | - | - |
| `succeeded` | Verified terminal success. | Fulfil exactly once. |
| `failed` | Verified provider failure. | Mark the pending order failed; never fulfil. |
| `cancelled` | Verified cancellation. | Mark it cancelled; never fulfil. |
| `pending` | Payment is not terminal yet. | Keep it pending and wait for later provider evidence. |
| `unknown` | Evidence was accepted but UPG cannot safely determine a final state. | Keep it unfulfilled and send it to reconciliation/support review. |

**Your application code — handle the typed SDK result.** The order methods below are examples of methods you implement in your own application; they are not AvraAPI SDK methods.

<Tabs>
  <Tab title="PHP">
    ```php theme={null}
    if (! $result->verified) {
        $orders->markCompletionNeedsReview($pendingOrder->id, $result->requestId);
        return;
    }

    switch ($result->paymentStatus) {
        case PaymentStatus::Succeeded:
            $orders->markPaidIfPending($pendingOrder->id, $result->gatewayReference, $result->requestId);
            fulfilOrderOnce($pendingOrder); // Your application code.
            break;

        case PaymentStatus::Failed:
            $orders->markPaymentFailedIfPending($pendingOrder->id, $result->providerStatus, $result->requestId);
            break;

        case PaymentStatus::Cancelled:
            $orders->markPaymentCancelledIfPending($pendingOrder->id, $result->providerStatus, $result->requestId);
            break;

        case PaymentStatus::Pending:
            $orders->keepPaymentPending($pendingOrder->id, $result->requestId);
            break;

        case PaymentStatus::Unknown:
            $orders->markCompletionNeedsReview($pendingOrder->id, $result->requestId);
            break;
    }
    ```
  </Tab>

  <Tab title="Laravel">
    ```php theme={null}
    if (! $result->verified) {
        $orders->markCompletionNeedsReview($pendingOrder->id, $result->requestId);
        return;
    }

    switch ($result->paymentStatus) {
        case PaymentStatus::Succeeded:
            $orders->markPaidIfPending($pendingOrder->id, $result->gatewayReference, $result->requestId);
            fulfilOrderOnce($pendingOrder); // Your application code.
            break;

        case PaymentStatus::Failed:
            $orders->markPaymentFailedIfPending($pendingOrder->id, $result->providerStatus, $result->requestId);
            break;

        case PaymentStatus::Cancelled:
            $orders->markPaymentCancelledIfPending($pendingOrder->id, $result->providerStatus, $result->requestId);
            break;

        case PaymentStatus::Pending:
            $orders->keepPaymentPending($pendingOrder->id, $result->requestId);
            break;

        case PaymentStatus::Unknown:
            $orders->markCompletionNeedsReview($pendingOrder->id, $result->requestId);
            break;
    }
    ```
  </Tab>

  <Tab title="Node.js">
    ```ts theme={null}
    import { PaymentStatus } from '@avraapi/node-sdk';

    if (!result.verified) {
      await orders.markCompletionNeedsReview(pendingOrder.id, result.requestId);
    } else if (result.paymentStatus === PaymentStatus.Succeeded) {
      await orders.markPaidIfPending(pendingOrder.id, result.gatewayReference, result.requestId);
      await fulfilOrderOnce(pendingOrder);
    } else if ([PaymentStatus.Pending, PaymentStatus.Unknown].includes(result.paymentStatus)) {
      await orders.keepPaymentPending(pendingOrder.id, result.requestId);
    } else {
      await orders.markPaymentFailedIfPending(pendingOrder.id, result.providerStatus, result.requestId);
    }
    ```
  </Tab>
</Tabs>

<Note>
  The raw AvraAPI transport response can have `success: true` while `data.payment_status` is `failed`, `cancelled`, `pending`, or `unknown`. `success` describes the completion request; `payment_status` describes the payment outcome.
</Note>

## Complete development guideline

### 1. Initialize the SDK

Keep the project Client ID and Client Secret in server environment variables. Never initialize this client in a browser bundle.

<Tabs>
  <Tab title="PHP">
    ```php theme={null}
    use Avraapi\Apix\ApixClient;

    $apix = new ApixClient([
        'apiKey' => $_ENV['APIX_PROJECT_KEY'],
        'apiSecret' => $_ENV['APIX_API_SECRET'],
        'env' => 'dev', // Use your project environment.
    ]);
    ```
  </Tab>

  <Tab title="Laravel">
    ```php theme={null}
    // .env
    APIX_PROJECT_KEY=your-project-client-id
    APIX_API_SECRET=your-project-client-secret
    APIX_ENV=dev

    use Avraapi\Laravel\Facades\AvraAPI;
    ```
  </Tab>

  <Tab title="Node.js">
    ```ts theme={null}
    import { ApixClient } from '@avraapi/node-sdk';

    const client = new ApixClient({
      projectKey: process.env.APIX_PROJECT_KEY,
      apiSecret: process.env.APIX_API_SECRET,
      env: 'dev',
    });
    ```
  </Tab>
</Tabs>

### 2. Add the checkout page with Payment Elements

Payment Elements is the recommended quick-start UI because it renders the customer form, shows only server-discovered payment methods, handles unavailable states, and launches the correct public checkout presentation.

The PHP page below obtains browser-safe availability data on the server. It does not expose the SDK credentials, Vault configuration, Workspace data, or gateway secrets.

**Your application code — this page includes one real `availability()` SDK call.** Replace the markup, styling, product display, and `/payments/checkout-session` URL with your own application design.

<Tabs>
  <Tab title="PHP">
    ```php theme={null}
    <?php
    // Your Blade checkout view, supplied with server-side availability data.
    $merchantDomain = 'shop.example.com';
    $availability = $apix->payment()
        ->availability($merchantDomain)
        ->toElementsPayload();

    $availabilityJson = json_encode(
        $availability,
        JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_THROW_ON_ERROR,
    );
    ?>

    <div id="payment-customer"></div>
    <div id="payment-methods"></div>
    <button id="payment-submit" type="button">Continue to payment</button>
    <p id="payment-message" role="status"></p>

    <script
      src="https://cdn.avraapi.com/payment-elements/v1.1.2/avraapi-payment-elements.umd.js"
      integrity="sha384-OFlOd6fgBnHYPoVCXZ9AbKa1VH4GalDBS6fK1Tqib1wecbf2aJ0mMpmJOrma+aSt"
      crossorigin="anonymous"
      defer
    ></script>

    <script
      id="payment-methods--avraapi-payment-availability"
      type="application/json"
      data-avraapi-payment-availability-for="payment-methods"
    ><?= $availabilityJson ?></script>

    <script>
    window.addEventListener('DOMContentLoaded', () => {
      const message = document.getElementById('payment-message');
      const button = document.getElementById('payment-submit');

      const elements = AvraAPIPaymentElements.create({
        createOrder: async ({ gateway, mode, customer, providerOptions }) => {
          const response = await fetch('/payments/checkout-session', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ gateway, mode, customer, providerOptions }),
          });

          const session = await response.json();
          if (!response.ok) {
            throw new Error(session.message || 'Checkout could not be prepared.');
          }

          return session;
        },
        onError: (error) => {
          message.textContent = error.message || 'Checkout could not be started.';
          button.disabled = false;
        },
        onStateChange: (event) => {
          if (event.type === 'checkout_started') {
            message.textContent = 'Opening secure checkout…';
          }

          if (event.type === 'checkout_cancelled' || event.type === 'checkout_error') {
            button.disabled = false;
          }
        },
      });

      elements.renderForm('#payment-customer', {
        defaultCountry: 'LK',
        phoneDefaultCountry: 'LK',
        syncPhoneCountry: true,
      });

      // Methods are read from the server-generated availability bootstrap above.
      elements.renderMethods('#payment-methods');

      button.addEventListener('click', async () => {
        button.disabled = true;
        message.textContent = 'Preparing secure checkout…';

        try {
          await elements.checkout();
        } catch (_) {
          // onError restores the UI with a customer-safe message.
        }
      });
    });
    </script>
    ```
  </Tab>

  <Tab title="Laravel">
    ```php theme={null}
    <?php
    use Avraapi\Laravel\Facades\AvraAPI;

    // Your checkout page, rendered by your PHP application.
    $merchantDomain = 'shop.example.com';
    $availability = AvraAPI::payment()
        ->availability($merchantDomain)
        ->toElementsPayload();

    $availabilityJson = json_encode(
        $availability,
        JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_THROW_ON_ERROR,
    );
    ?>

    <div id="payment-customer"></div>
    <div id="payment-methods"></div>
    <button id="payment-submit" type="button">Continue to payment</button>
    <p id="payment-message" role="status"></p>

    <script
      src="https://cdn.avraapi.com/payment-elements/v1.1.2/avraapi-payment-elements.umd.js"
      integrity="sha384-OFlOd6fgBnHYPoVCXZ9AbKa1VH4GalDBS6fK1Tqib1wecbf2aJ0mMpmJOrma+aSt"
      crossorigin="anonymous"
      defer
    ></script>

    <script
      id="payment-methods--avraapi-payment-availability"
      type="application/json"
      data-avraapi-payment-availability-for="payment-methods"
    ><?= $availabilityJson ?></script>

    <script>
    window.addEventListener('DOMContentLoaded', () => {
      const message = document.getElementById('payment-message');
      const button = document.getElementById('payment-submit');

      const elements = AvraAPIPaymentElements.create({
        createOrder: async ({ gateway, mode, customer, providerOptions }) => {
          const response = await fetch('/payments/checkout-session', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ gateway, mode, customer, providerOptions }),
          });

          const session = await response.json();
          if (!response.ok) {
            throw new Error(session.message || 'Checkout could not be prepared.');
          }

          return session;
        },
        onError: (error) => {
          message.textContent = error.message || 'Checkout could not be started.';
          button.disabled = false;
        },
        onStateChange: (event) => {
          if (event.type === 'checkout_started') {
            message.textContent = 'Opening secure checkout…';
          }

          if (event.type === 'checkout_cancelled' || event.type === 'checkout_error') {
            button.disabled = false;
          }
        },
      });

      elements.renderForm('#payment-customer', {
        defaultCountry: 'LK',
        phoneDefaultCountry: 'LK',
        syncPhoneCountry: true,
      });

      // Methods are read from the server-generated availability bootstrap above.
      elements.renderMethods('#payment-methods');

      button.addEventListener('click', async () => {
        button.disabled = true;
        message.textContent = 'Preparing secure checkout…';

        try {
          await elements.checkout();
        } catch (_) {
          // onError restores the UI with a customer-safe message.
        }
      });
    });
    </script>
    ```
  </Tab>

  <Tab title="Node.js">
    ```ts theme={null}
    // Your Node.js server route: the SDK remains on the server.
    const availability = await client.payment().availability('shop.example.com');
    const availabilityJson = JSON.stringify(availability.toElementsPayload())
      .replace(/</g, '\\u003c')
      .replace(/>/g, '\\u003e')
      .replace(/&/g, '\\u0026');

    response.type('html').send(`
      <div id="payment-customer"></div><div id="payment-methods"></div>
      <button id="payment-submit" type="button">Continue to payment</button>
      <script
        src="https://cdn.avraapi.com/payment-elements/v1.1.2/avraapi-payment-elements.umd.js"
        integrity="sha384-OFlOd6fgBnHYPoVCXZ9AbKa1VH4GalDBS6fK1Tqib1wecbf2aJ0mMpmJOrma+aSt"
        crossorigin="anonymous"
        defer
      ></script>
      <script id="payment-methods--avraapi-payment-availability" type="application/json"
        data-avraapi-payment-availability-for="payment-methods">${availabilityJson}</script>
    `);
    ```
  </Tab>
</Tabs>

<Note>
  The browser sends the selected `gateway`, `mode`, customer fields, and non-secret `providerOptions` to **your** checkout-session endpoint. Your server must validate the gateway/mode again against a fresh `availability()` result. Browser input is only a request, never permission to use a gateway.
</Note>

For styling, custom labels, DirectPay container placement, events, and unavailable-state behaviour, see [Payment Elements](/universal-payment-gateway/payment-elements/overview).

### 3. Create the payment session on your server

Your backend checkout endpoint must calculate the order amount from trusted server data, validate the chosen gateway and mode against availability, create the session, and persist `completionContext` with your pending order. Do not accept item prices, currency, URLs, or gateway environment from the browser.

**Combined reference — your application code plus real AvraAPI SDK calls.** Replace only the order-storage and product-pricing portions marked in the code; retain the availability validation and `createOrder()` structure.

<Tabs>
  <Tab title="PHP">
    ```php theme={null}
    <?php
    declare(strict_types=1);

    use Avraapi\Apix\Payments\CheckoutMode;
    use Avraapi\Apix\Payments\CreateOrderOptions;
    use Avraapi\Apix\Payments\GatewayCode;

    // Example backend endpoint: POST /payments/checkout-session
    $input = json_decode((string) file_get_contents('php://input'), true, 512, JSON_THROW_ON_ERROR);

    /* Your application code: validate this request before using it. */
    $requestedGateway = is_string($input['gateway'] ?? null) ? $input['gateway'] : '';
    $requestedMode = is_string($input['mode'] ?? null) ? $input['mode'] : '';
    $customer = is_array($input['customer'] ?? null) ? $input['customer'] : [];
    $providerOptions = is_array($input['providerOptions'] ?? null) ? $input['providerOptions'] : [];
    $merchantDomain = 'shop.example.com'; // Trusted application configuration.

    // AvraAPI SDK call — real availability() method.
    $availability = $apix->payment()->availability($merchantDomain);
    if (! $availability->isReady()) {
        http_response_code(409);
        echo json_encode(['message' => $availability->message ?? 'Payments are unavailable.']);
        exit;
    }

    $selectedMethod = null;
    foreach ($availability->methods() as $method) {
        if (($method['gateway'] ?? null) === $requestedGateway
            && ($method['mode'] ?? null) === $requestedMode
            && ($method['available'] ?? true) !== false) {
            $selectedMethod = $method;
            break;
        }
    }

    if ($selectedMethod === null) {
        http_response_code(422);
        echo json_encode(['message' => 'The selected payment method is unavailable.']);
        exit;
    }

    /*
     * Your application code — replace this whole commercial section with
     * your own durable pending-order transaction and trusted product catalogue.
     * Never take item pricing, currency, URLs, or environment from the browser.
     */
    $merchantOrderId = 'ORDER-'.strtoupper(bin2hex(random_bytes(8)));
    $gatewayCode = (string) $selectedMethod['gateway'];
    $items = 'Premium membership';
    $amount = '1500.00';
    $currency = 'LKR';
    $urls = [
        // Configure these gateway-scoped HTTPS URLs to route to one shared handler.
        // The configured Vault defaults must allow the values required by each gateway.
        'return_url' => "https://shop.example.com/payments/return/{$gatewayCode}",
        'cancel_url' => "https://shop.example.com/payments/cancel/{$gatewayCode}",
        'notify_url' => "https://shop.example.com/payments/callback/{$gatewayCode}",
    ];

    // AvraAPI SDK call — real createOrder() method.
    $session = $apix->payment()->createOrder(new CreateOrderOptions(
        gateway: GatewayCode::from($selectedMethod['gateway']),
        mode: CheckoutMode::from($selectedMethod['mode']),
        orderId: $merchantOrderId,
        items: $items,
        amount: $amount,
        currency: $currency,
        customer: $customer,
        urls: $urls,
        merchantDomain: $merchantDomain,
        providerOptions: $providerOptions,
    ));

    /*
     * Persist this in your own pending-order transaction before responding:
     * - $merchantOrderId, $session->gateway->value, $session->mode->value
     * - $amount, $currency, and $session->completionContext
     * - $session->requestId and $session->expiresAt for support/reconciliation
     */

    http_response_code(200);
    header('Content-Type: application/json; charset=UTF-8');

    // Return only browser-safe session data. Never return completionContext.
    echo json_encode([
        'gateway' => $session->gateway->value,
        'mode' => $session->mode->value,
        'status' => $session->status,
        'expires_at' => $session->expiresAt,
        'checkout' => $session->checkout,
        'request_id' => $session->requestId,
    ], JSON_THROW_ON_ERROR);
    ```
  </Tab>

  <Tab title="Laravel">
    ```php theme={null}
    <?php
    use Avraapi\Laravel\Facades\AvraAPI;

    declare(strict_types=1);

    use Avraapi\Apix\Payments\CheckoutMode;
    use Avraapi\Apix\Payments\CreateOrderOptions;
    use Avraapi\Apix\Payments\GatewayCode;

    // Example backend endpoint: POST /payments/checkout-session
    $input = json_decode((string) file_get_contents('php://input'), true, 512, JSON_THROW_ON_ERROR);

    /* Your application code: validate this request before using it. */
    $requestedGateway = is_string($input['gateway'] ?? null) ? $input['gateway'] : '';
    $requestedMode = is_string($input['mode'] ?? null) ? $input['mode'] : '';
    $customer = is_array($input['customer'] ?? null) ? $input['customer'] : [];
    $providerOptions = is_array($input['providerOptions'] ?? null) ? $input['providerOptions'] : [];
    $merchantDomain = 'shop.example.com'; // Trusted application configuration.

    // AvraAPI SDK call — real availability() method.
    $availability = AvraAPI::payment()->availability($merchantDomain);
    if (! $availability->isReady()) {
        http_response_code(409);
        echo json_encode(['message' => $availability->message ?? 'Payments are unavailable.']);
        exit;
    }

    $selectedMethod = null;
    foreach ($availability->methods() as $method) {
        if (($method['gateway'] ?? null) === $requestedGateway
            && ($method['mode'] ?? null) === $requestedMode
            && ($method['available'] ?? true) !== false) {
            $selectedMethod = $method;
            break;
        }
    }

    if ($selectedMethod === null) {
        http_response_code(422);
        echo json_encode(['message' => 'The selected payment method is unavailable.']);
        exit;
    }

    /*
     * Your application code — replace this whole commercial section with
     * your own durable pending-order transaction and trusted product catalogue.
     * Never take item pricing, currency, URLs, or environment from the browser.
     */
    $merchantOrderId = 'ORDER-'.strtoupper(bin2hex(random_bytes(8)));
    $gatewayCode = (string) $selectedMethod['gateway'];
    $items = 'Premium membership';
    $amount = '1500.00';
    $currency = 'LKR';
    $urls = [
        // Configure these gateway-scoped HTTPS URLs to route to one shared handler.
        // The configured Vault defaults must allow the values required by each gateway.
        'return_url' => "https://shop.example.com/payments/return/{$gatewayCode}",
        'cancel_url' => "https://shop.example.com/payments/cancel/{$gatewayCode}",
        'notify_url' => "https://shop.example.com/payments/callback/{$gatewayCode}",
    ];

    // AvraAPI SDK call — real createOrder() method.
    $session = AvraAPI::payment()->createOrder(new CreateOrderOptions(
        gateway: GatewayCode::from($selectedMethod['gateway']),
        mode: CheckoutMode::from($selectedMethod['mode']),
        orderId: $merchantOrderId,
        items: $items,
        amount: $amount,
        currency: $currency,
        customer: $customer,
        urls: $urls,
        merchantDomain: $merchantDomain,
        providerOptions: $providerOptions,
    ));

    /*
     * Persist this in your own pending-order transaction before responding:
     * - $merchantOrderId, $session->gateway->value, $session->mode->value
     * - $amount, $currency, and $session->completionContext
     * - $session->requestId and $session->expiresAt for support/reconciliation
     */

    http_response_code(200);
    header('Content-Type: application/json; charset=UTF-8');

    // Return only browser-safe session data. Never return completionContext.
    echo json_encode([
        'gateway' => $session->gateway->value,
        'mode' => $session->mode->value,
        'status' => $session->status,
        'expires_at' => $session->expiresAt,
        'checkout' => $session->checkout,
        'request_id' => $session->requestId,
    ], JSON_THROW_ON_ERROR);
    ```
  </Tab>

  <Tab title="Node.js">
    ```ts theme={null}
    import { CheckoutMode, CreateOrderOptions, GatewayCode } from '@avraapi/node-sdk';

    // Example Node.js route: POST /payments/checkout-session
    const { gateway, mode, customer, providerOptions = {} } = request.body;
    const merchantDomain = 'shop.example.com'; // Trusted server configuration.
    const availability = await client.payment().availability(merchantDomain);
    const selected = availability.methods.find((method) =>
      method.gateway === gateway && method.mode === mode && method.available,
    );
    if (!availability.isReady() || !selected) {
      return response.status(422).json({ message: 'The selected payment method is unavailable.' });
    }

    const orderId = `ORDER-${crypto.randomUUID()}`; // Create and persist your pending order.
    const session = await client.payment().createOrder(new CreateOrderOptions({
      gateway: gateway as GatewayCode, mode: mode as CheckoutMode, orderId,
      items: 'Premium membership', amount: '1500.00', currency: 'LKR',
      customer, providerOptions, merchantDomain,
      urls: {
        return_url: `https://shop.example.com/payments/return/${gateway}`,
        cancel_url: `https://shop.example.com/payments/cancel/${gateway}`,
        notify_url: `https://shop.example.com/payments/callback/${gateway}`,
      },
    }));

    await orders.savePending({ orderId, completionContext: session.completionContext });
    return response.json({ gateway: session.gateway, mode: session.mode, checkout: session.checkout });
    ```
  </Tab>
</Tabs>

### What Payment Elements does next

After your endpoint returns the public session, Payment Elements chooses the correct presentation automatically:

| Returned checkout type | Browser action |
| - | - |
| `redirect` | Redirects the buyer to the provider’s validated HTTPS URL. |
| `redirect_form` | Posts the signed provider form. |
| `overlay` | Opens the PayHere provider overlay. |
| `onepay_sdk_overlay` | Opens the OnePay SDK overlay. |
| `directpay_v3` | Opens DirectPay Overlay or mounts its Embedded checkout according to session mode. |

For a fully custom browser UI without Payment Elements, use [Redirect Checkout](/universal-payment-gateway/advanced-setup/redirect-checkout) or [In-page Checkout](/universal-payment-gateway/advanced-setup/hosted-checkout). Those pages document the exact SDK-only handoff for each public checkout type.

### 4. Complete the payment in the shared handler

The shared handler receives the gateway from its server-defined route or query parameter, then loads the exact pending merchant order and its saved `completionContext`. It uses the matching gateway-specific provider callback payload wrapper before calling `completePayment()`. Do not fulfil an order from a browser redirect, overlay event, or frontend “success” message.

**Combined reference — your application code plus the real `completePayment()` SDK call.** The example below uses the simple query-parameter routing style. With route-based routing, obtain the same gateway value from your framework route parameter instead.

<Tabs>
  <Tab title="PHP">
    ```php theme={null}
    use Avraapi\Apix\Payments\GatewayCode;
    use Avraapi\Apix\Payments\PaymentCompletionOptions;
    use Avraapi\Apix\Payments\PaymentResponseOptions;
    use Avraapi\Apix\Payments\PaymentStatus;

    /*
     * Your application code — read this only from the configured URL.
     * This query-parameter example intentionally uses $_GET, never $_REQUEST.
     * A route-based application should read its validated {gateway} route parameter.
     */
    $gatewayCode = is_string($_GET['gateway'] ?? null) ? $_GET['gateway'] : '';
    $gateway = GatewayCode::from($gatewayCode);

    // Your application code — load the pending order using callback correlation.
    $pendingOrder = findPendingOrderFromProviderCallback($gateway);

    // Your application code — preserve raw bodies/signature headers where required.
    $providerPayload = buildGatewaySpecificProviderPayload($gateway, $pendingOrder);

    // AvraAPI SDK call — real completePayment() method.
    $result = $apix->payment()->completePayment(new PaymentCompletionOptions(
        gateway: $gateway,
        completionContext: $pendingOrder->payment_completion_context,
        payload: $providerPayload,
        // Optional: omitting this uses the default short() response mode.
        response: PaymentResponseOptions::short(),
    ));

    if ($result->verified && $result->paymentStatus === PaymentStatus::Succeeded) {
        // Your application code — fulfil only once, idempotently.
        markOrderPaidAndFulfilOnce(
            $pendingOrder,
            $result->gatewayReference,
            $result->requestId,
        );
    } elseif ($result->paymentStatus === PaymentStatus::Failed) {
        // Your application code — update the order; never fulfil it.
        markOrderPaymentFailed($pendingOrder, $result->providerStatus, $result->requestId);
    } elseif ($result->paymentStatus === PaymentStatus::Cancelled) {
        // Your application code — update the order; never fulfil it.
        markOrderPaymentCancelled($pendingOrder, $result->providerStatus, $result->requestId);
    } else {
        // pending, unknown, or unverified: retain the order for later evidence or review.
        markOrderPaymentPendingOrNeedsReview($pendingOrder, $result->requestId);
    }
    ```
  </Tab>

  <Tab title="Laravel">
    ```php theme={null}
    use Avraapi\Laravel\Facades\AvraAPI;

    use Avraapi\Apix\Payments\GatewayCode;
    use Avraapi\Apix\Payments\PaymentCompletionOptions;
    use Avraapi\Apix\Payments\PaymentResponseOptions;
    use Avraapi\Apix\Payments\PaymentStatus;

    /*
     * Your application code — read this only from the configured URL.
     * This query-parameter example intentionally uses $_GET, never $_REQUEST.
     * A route-based application should read its validated {gateway} route parameter.
     */
    $gatewayCode = is_string($_GET['gateway'] ?? null) ? $_GET['gateway'] : '';
    $gateway = GatewayCode::from($gatewayCode);

    // Your application code — load the pending order using callback correlation.
    $pendingOrder = findPendingOrderFromProviderCallback($gateway);

    // Your application code — preserve raw bodies/signature headers where required.
    $providerPayload = buildGatewaySpecificProviderPayload($gateway, $pendingOrder);

    // AvraAPI SDK call — real completePayment() method.
    $result = AvraAPI::payment()->completePayment(new PaymentCompletionOptions(
        gateway: $gateway,
        completionContext: $pendingOrder->payment_completion_context,
        payload: $providerPayload,
        // Optional: omitting this uses the default short() response mode.
        response: PaymentResponseOptions::short(),
    ));

    if ($result->verified && $result->paymentStatus === PaymentStatus::Succeeded) {
        // Your application code — fulfil only once, idempotently.
        markOrderPaidAndFulfilOnce(
            $pendingOrder,
            $result->gatewayReference,
            $result->requestId,
        );
    } elseif ($result->paymentStatus === PaymentStatus::Failed) {
        // Your application code — update the order; never fulfil it.
        markOrderPaymentFailed($pendingOrder, $result->providerStatus, $result->requestId);
    } elseif ($result->paymentStatus === PaymentStatus::Cancelled) {
        // Your application code — update the order; never fulfil it.
        markOrderPaymentCancelled($pendingOrder, $result->providerStatus, $result->requestId);
    } else {
        // pending, unknown, or unverified: retain the order for later evidence or review.
        markOrderPaymentPendingOrNeedsReview($pendingOrder, $result->requestId);
    }
    ```
  </Tab>

  <Tab title="Node.js">
    ```ts theme={null}
    import {
      GatewayCode, PaymentCompletionOptions, PaymentResponseOptions, PaymentStatus,
    } from '@avraapi/node-sdk';

    const gateway = request.params.gateway as GatewayCode; // Server-defined route value.
    const pendingOrder = await orders.findPendingFromCallback(gateway, request);
    const providerPayload = buildGatewaySpecificProviderPayload(gateway, request); // Your code.
    const result = await client.payment().completePayment(new PaymentCompletionOptions({
      gateway, completionContext: pendingOrder.completionContext, payload: providerPayload,
      response: PaymentResponseOptions.short(), reconcileProvider: true,
    }));

    if (result.verified && result.paymentStatus === PaymentStatus.Succeeded) {
      await orders.markPaidAndFulfilOnce(pendingOrder.id, result.gatewayReference, result.requestId);
    } else if ([PaymentStatus.Pending, PaymentStatus.Unknown].includes(result.paymentStatus)) {
      await orders.keepPending(pendingOrder.id, result.requestId);
    } else {
      await orders.markNotPaid(pendingOrder.id, result.providerStatus, result.requestId);
    }
    ```
  </Tab>
</Tabs>

`findPendingOrderFromProviderCallback()`, `buildGatewaySpecificProviderPayload()`, and `markOrderPaidAndFulfilOnce()` represent your own application’s order persistence, gateway dispatch, and fulfilment logic. They are deliberately not SDK methods. The SDK call itself is `completePayment()`.

**The typed SDK response — an example `short()` result represented as an array**

```php theme={null}
$safeCompletion = [
    'gateway' => 'payhere',
    'verified' => true,
    'payment_status' => 'succeeded',
    'provider_status' => '2',
    'order_id' => 'ORDER-2026-000184',
    'gateway_reference' => '320000000000',
    'amount' => '1500.00',
    'currency' => 'LKR',
    'request_id' => '01j...',
    'reconciliation' => [
        'attempted' => true,
        'state' => 'matched',
        'authority' => 'provider_status',
        'callback_status' => '2',
        'secondary_status' => 'RECEIVED',
        'retry_recommended' => false,
    ],
];
```

This is a server-side representation of the real `PaymentCompletionResult` properties, not a second API response and not data to return to the buyer. Gateway-specific values and reconciliation metadata vary by provider and completion path.

Use [Payment Response & Completion](/universal-payment-gateway/advanced-setup/webhooks-and-completion) for the response contract, `PaymentResponseOptions::short()`, `full()`, `include()`, reconciliation rules, and provider payload requirements.

## Quick Setup safety checklist

* Run `availability()` on the server and validate the buyer’s selected gateway/mode against it again during session creation.
* Route each provider’s configured callback/return URL into one shared completion handler with a server-defined gateway identifier; never guess the provider from an untrusted browser value.
* Calculate product, price, currency, callback URLs, merchant domain, and gateway environment on the server.
* Persist `completionContext` only with the pending merchant order; never return or log it.
* Return only `gateway`, `mode`, expiry, request ID, and `checkout` data to the browser.
* Treat Elements and provider browser events as UI signals, not payment proof.
* Use `completePayment()` and fulfil only a verified `PaymentStatus::Succeeded` result.
* Make payment completion and fulfilment idempotent because provider callbacks can be delivered more than once.

## Download sample code files

<Note>
  Downloadable, reviewed sample projects are being prepared. The packages below will be published only after their gateway contract tests and framework examples are complete.
</Note>

<CardGroup cols={3}>
  <Card title="PHP sample" icon="download" className="border-2 border-slate-800/60 hover:border-[#0450ff] transition-all duration-700 ease-out">
    Coming soon — a complete PHP checkout and shared completion-handler ZIP.
  </Card>

  <Card title="Laravel sample" icon="download" className="border-2 border-slate-800/60 hover:border-[#0450ff] transition-all duration-700 ease-out">
    Laravel SDK 1.2.0 is supported above. A downloadable controller, routes, jobs, and checkout example ZIP will be added separately.
  </Card>

  <Card title="Node.js sample" icon="download" href="/universal-payment-gateway/quick-setup" className="border-2 border-slate-800/60 hover:border-[#0450ff] transition-all duration-700 ease-out">
    Use the released Node.js server-side availability, create-order, and completion examples on this page.
  </Card>
</CardGroup>

## Next steps

* [Project Slots](/universal-payment-gateway/project-slots) — understand why a project needs an active UPG slot.
* [Gateway Vault](/universal-payment-gateway/gateway-vault) — configure provider credentials safely.
* [Advanced Setup](/universal-payment-gateway/advanced-setup/payment-initiation) — use custom checkout flows, response options, gateway environments, and advanced provider behaviour.
* [Payment Elements](/universal-payment-gateway/payment-elements/overview) — customise fields, methods, events, and unavailable states.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.