> ## 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.

# Payment Response & Completion

> Verify provider callbacks and returns on your server before fulfilling an order.

`completePayment()` turns authenticated provider evidence into one gateway-neutral payment result. It is the only result your application should use when deciding whether to fulfil an order.

The integration rules on this page apply to every AvraAPI SDK. Laravel 1.2.0 uses `AvraAPI::payment()` and Node.js SDK 1.2.0 uses `client.payment()` with the same typed payment options, completion result, and server-only safety rules.

<Warning>
  A redirect return, an overlay event, or a browser success screen is not payment proof. Do not mark an order paid until your backend receives and evaluates `completePayment()`.
</Warning>

## The completion sequence

1. Look up the pending order using a trusted provider reference or an opaque return token that your backend created.
2. Load the stored `completionContext` for that exact order.
3. Preserve provider callback or return data in its gateway-required format.
4. Call `completePayment()` from the webhook, callback, or server return handler.
5. Fulfil only a verified result with `paymentStatus === PaymentStatus::Succeeded`.
6. Make the order update idempotent because providers can send the same callback more than once.

## 1. Set the `GatewayCode`

Use the gateway stored with the pending order. Do not trust a browser query parameter to choose it.

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

    $gateway = GatewayCode::from($pendingOrder->gateway);
    ```
  </Tab>

  <Tab title="Laravel">
    ```php theme={null}
    use Avraapi\Apix\Payments\GatewayCode;

    $gateway = GatewayCode::from($pendingOrder->gateway);
    ```
  </Tab>

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

    const gateway = request.params.gateway as GatewayCode; // Server-defined route value.
    ```
  </Tab>
</Tabs>

## 2. Load the saved `completionContext`

The context returned by `createOrder()` is signed, server-only evidence that binds completion to one prepared payment session. Load it from protected order storage.

<Tabs>
  <Tab title="PHP">
    ```php theme={null}
    $completionContext = $pendingOrder->payment_completion_context;

    if ($completionContext === null || $completionContext === '') {
        throw new RuntimeException('This payment has no valid completion context.');
    }
    ```
  </Tab>

  <Tab title="Laravel">
    ```php theme={null}
    $completionContext = $pendingOrder->payment_completion_context;

    if ($completionContext === null || $completionContext === '') {
        throw new RuntimeException('This payment has no valid completion context.');
    }
    ```
  </Tab>

  <Tab title="Node.js">
    ```ts theme={null}
    const pendingOrder = await orders.findPendingFromCallback(gateway, request);
    const completionContext = pendingOrder.completionContext; // Never expose this value.
    ```
  </Tab>
</Tabs>

Never rebuild, guess, expose, or accept this value from a browser request.

## 3. Preserve the provider payload exactly

Each gateway has its own callback or return shape. Preserve raw bodies and signature headers where the provider requires them, then use the gateway-specific SDK payload wrapper. Do not parse and re-encode signed provider data before the wrapper receives it.

For the required payload type and exact callback/return handling for each gateway, see [Gateway-wise Functions](/universal-payment-gateway/gateway-wise-functions).

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

    // Stripe example: preserve the exact raw body and signature header.
    $rawBody = (string) file_get_contents('php://input');
    $signature = (string) ($_SERVER['HTTP_STRIPE_SIGNATURE'] ?? '');

    $payload = (new StripeWebhookPayload($rawBody, $signature))->toArray();
    ```
  </Tab>

  <Tab title="Laravel">
    ```php theme={null}
    use Avraapi\Apix\Payments\Stripe\StripeWebhookPayload;

    // Stripe example: preserve the exact raw body and signature header.
    $rawBody = (string) file_get_contents('php://input');
    $signature = (string) ($_SERVER['HTTP_STRIPE_SIGNATURE'] ?? '');

    $payload = (new StripeWebhookPayload($rawBody, $signature))->toArray();
    ```
  </Tab>

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

    const rawBody = await readRawRequestBody(request); // Your server helper.
    const payload = new DirectPayCallbackPayload(
      rawBody,
      String(request.headers.authorization ?? ''),
    ).toPayload();
    ```
  </Tab>
</Tabs>

## 4. Response option: `PaymentResponseOptions::short()`

`short()` is the default and recommended production response. It returns the normalized payment result needed for fulfilment without provider-native diagnostic bodies.

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

    $result = $apix->payment()->completePayment(new PaymentCompletionOptions(
        gateway: $gateway,
        completionContext: $completionContext,
        payload: $payload,
        response: PaymentResponseOptions::short(),
    ));
    ```
  </Tab>

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

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

    $result = AvraAPI::payment()->completePayment(new PaymentCompletionOptions(
        gateway: $gateway,
        completionContext: $completionContext,
        payload: $payload,
        response: PaymentResponseOptions::short(),
    ));
    ```
  </Tab>

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

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

### Actual response shape

This is the raw HTTP response envelope returned by AvraAPI **to the SDK transport**. Values such as order IDs, provider statuses, and references vary per real payment; the field names and nesting below are the actual completion contract.

```json theme={null}
{
  "success": true,
  "request_id": "01j...",
  "data": {
    "gateway": "payhere",
    "verified": true,
    "payment_status": "succeeded",
    "provider_status": "2",
    "order_id": "ORDER-2026-000184",
    "gateway_reference": "320000000000",
    "amount": "12000.00",
    "currency": "LKR",
    "reconciliation": {
      "attempted": true,
      "state": "matched",
      "authority": "provider_status",
      "callback_status": "2",
      "secondary_status": "RECEIVED",
      "retry_recommended": false
    }
  }
}
```

<Note>
  `success: true` means AvraAPI processed the completion request successfully. It does **not** mean the buyer paid. Always decide what to do from `data.verified` and `data.payment_status`.
</Note>

### Failed, cancelled, pending, and unknown responses

The normalized `payment_status` is the same across released gateways, while `provider_status` remains gateway-specific. The following are representative, verified PayHere `short()` envelopes. Other gateways use the same field names but return their own provider status values and reconciliation details.

<Tabs>
  <Tab title="Failed">
    ```json theme={null}
    {
      "success": true,
      "request_id": "01j...",
      "data": {
        "gateway": "payhere",
        "verified": true,
        "payment_status": "failed",
        "provider_status": "-2",
        "order_id": "ORDER-2026-000184",
        "gateway_reference": null,
        "amount": "12000.00",
        "currency": "LKR",
        "reconciliation": {
          "attempted": false,
          "state": "not_applicable",
          "authority": "callback",
          "callback_status": "-2",
          "secondary_status": null,
          "retry_recommended": false
        }
      }
    }
    ```

    The callback is authentic, but the payment failed. Mark the merchant order failed without fulfilment.
  </Tab>

  <Tab title="Cancelled">
    ```json theme={null}
    {
      "success": true,
      "request_id": "01j...",
      "data": {
        "gateway": "payhere",
        "verified": true,
        "payment_status": "cancelled",
        "provider_status": "-1",
        "order_id": "ORDER-2026-000184",
        "gateway_reference": null,
        "amount": "12000.00",
        "currency": "LKR",
        "reconciliation": {
          "attempted": false,
          "state": "not_applicable",
          "authority": "callback",
          "callback_status": "-1",
          "secondary_status": null,
          "retry_recommended": false
        }
      }
    }
    ```

    The callback is authentic, but the buyer cancelled the payment. Mark the merchant order cancelled without fulfilment.
  </Tab>

  <Tab title="Pending">
    ```json theme={null}
    {
      "success": true,
      "request_id": "01j...",
      "data": {
        "gateway": "payhere",
        "verified": true,
        "payment_status": "pending",
        "provider_status": "0",
        "order_id": "ORDER-2026-000184",
        "gateway_reference": null,
        "amount": "12000.00",
        "currency": "LKR",
        "reconciliation": {
          "attempted": false,
          "state": "not_applicable",
          "authority": "callback",
          "callback_status": "0",
          "secondary_status": null,
          "retry_recommended": false
        }
      }
    }
    ```

    Keep the merchant order pending. Wait for the provider's later evidence or follow that gateway's documented reconciliation flow.
  </Tab>

  <Tab title="Unknown">
    ```json theme={null}
    {
      "success": true,
      "request_id": "01j...",
      "data": {
        "gateway": "payhere",
        "verified": true,
        "payment_status": "unknown",
        "provider_status": "UNRECOGNISED_PROVIDER_STATUS",
        "order_id": "ORDER-2026-000184",
        "gateway_reference": null,
        "amount": "12000.00",
        "currency": "LKR",
        "reconciliation": {
          "attempted": false,
          "state": "not_applicable",
          "authority": "callback",
          "callback_status": "UNRECOGNISED_PROVIDER_STATUS",
          "secondary_status": null,
          "retry_recommended": false
        }
      }
    }
    ```

    Do not fulfil or automatically charge again. Keep the order unfulfilled and use a controlled reconciliation or support workflow.
  </Tab>
</Tabs>

### The object returned by `completePayment()`

SDK developers do **not** read `$response['data']`. The SDK reads the raw envelope internally, extracts `data` and `request_id`, then returns a `PaymentCompletionResult` object. SDK integrations use object properties such as `$result->amount` and `$result->paymentStatus->value`.

| Raw HTTP field | PHP SDK property |
| - | - |
| `data.gateway` | `$result->gateway->value` |
| `data.verified` | `$result->verified` |
| `data.payment_status` | `$result->paymentStatus->value` |
| `data.provider_status` | `$result->providerStatus` |
| `data.order_id` | `$result->orderId` |
| `data.gateway_reference` | `$result->gatewayReference` |
| `data.amount` | `$result->amount` |
| `data.currency` | `$result->currency` |
| top-level `request_id` | `$result->requestId` |
| `data.native_operations` | `$result->nativeOperations` |
| `data.include` | `$result->include` |
| `data.reconciliation` | `$result->reconciliation` |

For example, this is the gateway-neutral result that an application can deliberately create from the SDK object for its own protected order record:

<Tabs>
  <Tab title="PHP">
    ```php theme={null}
    $merchantResult = [
        '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,
    ];
    ```
  </Tab>

  <Tab title="Laravel">
    ```php theme={null}
    $merchantResult = [
        '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,
    ];
    ```
  </Tab>

  <Tab title="Node.js">
    ```ts theme={null}
    const safeCompletion = {
      gateway: result.gateway, verified: result.verified,
      payment_status: result.paymentStatus, 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,
    };
    ```
  </Tab>
</Tabs>

The resulting application-owned JSON can safely have this shape:

```json theme={null}
{
  "gateway": "payhere",
  "verified": true,
  "payment_status": "succeeded",
  "provider_status": "2",
  "order_id": "ORDER-2026-000184",
  "gateway_reference": "320000000000",
  "amount": "12000.00",
  "currency": "LKR",
  "request_id": "01j...",
  "reconciliation": {
    "attempted": true,
    "state": "matched",
    "authority": "provider_status",
    "callback_status": "2",
    "secondary_status": "RECEIVED",
    "retry_recommended": false
  }
}
```

## 5. Response option: `PaymentResponseOptions::full()`

`full()` adds the exact provider-native operation bodies under `native_operations`. Use it only in a protected server-side diagnostic workflow when there is a genuine integration or support need.

<Tabs>
  <Tab title="PHP">
    ```php theme={null}
    $diagnosticResult = $apix->payment()->completePayment(new PaymentCompletionOptions(
        gateway: $gateway,
        completionContext: $completionContext,
        payload: $payload,
        response: PaymentResponseOptions::full(),
    ));

    $nativeOperations = $diagnosticResult->nativeOperations;
    ```
  </Tab>

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

    $diagnosticResult = AvraAPI::payment()->completePayment(new PaymentCompletionOptions(
        gateway: $gateway,
        completionContext: $completionContext,
        payload: $payload,
        response: PaymentResponseOptions::full(),
    ));

    $nativeOperations = $diagnosticResult->nativeOperations;
    ```
  </Tab>

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

    const result = await client.payment().completePayment(new PaymentCompletionOptions({
      gateway, completionContext, payload, response: PaymentResponseOptions.full(),
    }));
    // Keep result.nativeOperations and provider-native data on the server only.
    ```
  </Tab>
</Tabs>

### Actual additional response data

`full()` preserves the normalized fields from `short()` and adds `native_operations`. The following is the actual PayHere callback operation structure used by the response projector; provider-native fields differ by gateway and flow.

```json theme={null}
{
  "success": true,
  "request_id": "01j...",
  "data": {
    "gateway": "payhere",
    "verified": true,
    "payment_status": "succeeded",
    "provider_status": "2",
    "order_id": "ORDER-2026-000184",
    "gateway_reference": "320000000000",
    "amount": "12000.00",
    "currency": "LKR",
    "reconciliation": {
      "attempted": true,
      "state": "matched",
      "authority": "provider_status",
      "callback_status": "2",
      "secondary_status": "RECEIVED",
      "retry_recommended": false
    },
    "native_operations": {
      "callback": {
        "card_no": "************4564",
        "status_code": "2"
      }
    }
  }
}
```

In the PHP SDK, read this additional data through `$diagnosticResult->nativeOperations`, not through a `data` array.

<Warning>
  `full()` can contain provider-native customer, masked-card, or detailed transaction fields. Do not return it to a browser, public return page, logs, analytics, cache, queue payload, or unapproved support tool.
</Warning>

## 6. Response option: `PaymentResponseOptions::include()`

`include()` returns only exact provider-native paths requested by your server. It is safer than `full()` when a protected workflow needs a small known set of fields.

<Tabs>
  <Tab title="PHP">
    ```php theme={null}
    $selectedResult = $apix->payment()->completePayment(new PaymentCompletionOptions(
        gateway: $gateway,
        completionContext: $completionContext,
        payload: $payload,
        response: PaymentResponseOptions::include([
            'callback.card_no',
            'callback.status_code',
        ]),
    ));

    $includedFields = $selectedResult->include;
    ```
  </Tab>

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

    $selectedResult = AvraAPI::payment()->completePayment(new PaymentCompletionOptions(
        gateway: $gateway,
        completionContext: $completionContext,
        payload: $payload,
        response: PaymentResponseOptions::include([
            'callback.card_no',
            'callback.status_code',
        ]),
    ));

    $includedFields = $selectedResult->include;
    ```
  </Tab>

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

    const result = await client.payment().completePayment(new PaymentCompletionOptions({
      gateway, completionContext, payload,
      response: PaymentResponseOptions.include(['callback.card_no', 'callback.status_code']),
    }));
    ```
  </Tab>
</Tabs>

### Actual additional response data

`include()` preserves the normalized result, returns only selected provider data in `native_operations`, and reports exactly which requested paths were recognised.

```json theme={null}
{
  "success": true,
  "request_id": "01j...",
  "data": {
    "gateway": "payhere",
    "verified": true,
    "payment_status": "succeeded",
    "provider_status": "2",
    "order_id": "ORDER-2026-000184",
    "gateway_reference": "320000000000",
    "amount": "12000.00",
    "currency": "LKR",
    "native_operations": {
      "callback": {
        "card_no": "************4564",
        "status_code": "2"
      }
    },
    "include": {
      "requested": [
        "callback.card_no",
        "callback.status_code"
      ],
      "included": [
        "callback.card_no",
        "callback.status_code"
      ],
      "unrecognized": []
    }
  }
}
```

Include paths must be non-empty strings and are valid only for `include()` mode. Use only paths documented for that gateway, and apply the same server-only handling rules as `full()`.

In the PHP SDK, the metadata is available through `$selectedResult->include` and the selected operation body through `$selectedResult->nativeOperations`.

## 7. `reconcileProvider` option

`reconcileProvider` is an optional boolean on `PaymentCompletionOptions`. It defaults to `true`. When a gateway supports a secondary authenticated status observation, UPG compares it with the callback or return evidence to reduce mismatches.

<Warning>
  Do not disable reconciliation for speed because a buyer returned to your site, closed a checkout window, or your frontend wants an earlier response. Browser events are never authoritative payment proof.
</Warning>

### Enabled — default and recommended

<Tabs>
  <Tab title="PHP">
    ```php theme={null}
    $result = $apix->payment()->completePayment(new PaymentCompletionOptions(
        gateway: $gateway,
        completionContext: $completionContext,
        payload: $payload,
        response: PaymentResponseOptions::short(),
        reconcileProvider: true,
    ));
    ```
  </Tab>

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

    $result = AvraAPI::payment()->completePayment(new PaymentCompletionOptions(
        gateway: $gateway,
        completionContext: $completionContext,
        payload: $payload,
        response: PaymentResponseOptions::short(),
        reconcileProvider: true,
    ));
    ```
  </Tab>

  <Tab title="Node.js">
    ```ts theme={null}
    const result = await client.payment().completePayment({
      gateway, completionContext, payload, reconcileProvider: true,
    }); // Default and recommended.
    ```
  </Tab>
</Tabs>

When a supported secondary provider read agrees with authenticated callback evidence, the actual reconciliation field has this shape:

```json theme={null}
{
  "reconciliation": {
    "attempted": true,
    "state": "matched",
    "authority": "provider_status",
    "callback_status": "2",
    "secondary_status": "RECEIVED",
    "retry_recommended": false
  }
}
```

If reconciliation conflicts, is unavailable, or the final state remains `pending` or `unknown`, do not fulfil. Retain the pending order and follow the gateway's retry or support process.

### Disabled — only where the gateway permits it

<Tabs>
  <Tab title="PHP">
    ```php theme={null}
    $result = $apix->payment()->completePayment(new PaymentCompletionOptions(
        gateway: $gateway,
        completionContext: $completionContext,
        payload: $payload,
        response: PaymentResponseOptions::short(),
        reconcileProvider: false,
    ));
    ```
  </Tab>

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

    $result = AvraAPI::payment()->completePayment(new PaymentCompletionOptions(
        gateway: $gateway,
        completionContext: $completionContext,
        payload: $payload,
        response: PaymentResponseOptions::short(),
        reconcileProvider: false,
    ));
    ```
  </Tab>

  <Tab title="Node.js">
    ```ts theme={null}
    const result = await client.payment().completePayment({
      gateway, completionContext, payload, reconcileProvider: false,
    }); // Use only where this gateway's guide explicitly permits it.
    ```
  </Tab>
</Tabs>

For a permitted PayHere signed-callback flow, disabling the optional retrieval produces this actual reconciliation shape:

```json theme={null}
{
  "reconciliation": {
    "attempted": false,
    "state": "not_applicable",
    "authority": "callback",
    "callback_status": "2",
    "secondary_status": null,
    "retry_recommended": false
  }
}
```

This flag never bypasses signature verification or a mandatory provider lifecycle. In particular, WebXPay, OnePay, and Stripe always perform their required provider verification; MarxPay uses its mandatory provider completion lifecycle. Gateway-wise pages will document gateway-specific rules for PayHere, PayPlus, KOKO, and all future adapters.

## 8. Handle every outcome and fulfil exactly once

Fulfil only a verified terminal success. Your order update must be idempotent so duplicated callbacks cannot create duplicate delivery, credit, invoice, or balance changes. A verified failed or cancelled result is still useful business evidence, but it must never trigger fulfilment.

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

    // Your application code — these order methods are not SDK methods.
    if (! $result->verified) {
        $orders->markCompletionNeedsReview($pendingOrder->id, $result->requestId);
        return;
    }

    switch ($result->paymentStatus) {
        case PaymentStatus::Succeeded:
            $orders->markPaidIfPending(
                orderId: $pendingOrder->id,
                gatewayReference: $result->gatewayReference,
                avraApiRequestId: $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}
    use Avraapi\Apix\Payments\PaymentStatus;

    // Your application code — these order methods are not SDK methods.
    if (! $result->verified) {
        $orders->markCompletionNeedsReview($pendingOrder->id, $result->requestId);
        return;
    }

    switch ($result->paymentStatus) {
        case PaymentStatus::Succeeded:
            $orders->markPaidIfPending(
                orderId: $pendingOrder->id,
                gatewayReference: $result->gatewayReference,
                avraApiRequestId: $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 && 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>

For `failed` and `cancelled`, update the pending order without fulfilment. For `pending` and `unknown`, retain the order and wait for a webhook, retry according to the provider's rules, or send it to a controlled reconciliation workflow.

## Handle completion errors without fulfilment

The SDK throws typed exceptions for unsuccessful HTTP responses. Catch them in your backend, record only safe correlation details, and leave the order unfulfilled.

<Tabs>
  <Tab title="PHP">
    ```php theme={null}
    use Avraapi\Apix\Exceptions\PaymentAccessException;
    use Avraapi\Apix\Exceptions\PaymentProviderException;
    use Avraapi\Apix\Exceptions\PaymentVerificationException;

    try {
        $result = $apix->payment()->completePayment($completionOptions);
    } catch (PaymentVerificationException $exception) {
        $orders->markCompletionRejected($pendingOrder->id);

        http_response_code(422);
        exit('Payment evidence could not be verified.');
    } catch (PaymentProviderException | PaymentAccessException $exception) {
        $orders->markCompletionNeedsReview($pendingOrder->id);

        // A transient completion failure can be retried by the provider.
        http_response_code(503);
        exit('Payment completion is temporarily unavailable.');
    }
    ```
  </Tab>

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

    use Avraapi\Apix\Exceptions\PaymentAccessException;
    use Avraapi\Apix\Exceptions\PaymentProviderException;
    use Avraapi\Apix\Exceptions\PaymentVerificationException;

    try {
        $result = AvraAPI::payment()->completePayment($completionOptions);
    } catch (PaymentVerificationException $exception) {
        $orders->markCompletionRejected($pendingOrder->id);

        http_response_code(422);
        exit('Payment evidence could not be verified.');
    } catch (PaymentProviderException | PaymentAccessException $exception) {
        $orders->markCompletionNeedsReview($pendingOrder->id);

        // A transient completion failure can be retried by the provider.
        http_response_code(503);
        exit('Payment completion is temporarily unavailable.');
    }
    ```
  </Tab>

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

    try {
      await client.payment().completePayment({ gateway, completionContext, payload });
    } catch (error) {
      if (error instanceof PaymentVerificationError) return response.status(400).end();
      if (error instanceof PaymentProviderError || error instanceof ApixError) {
        await orders.keepPending(pendingOrder.id, error.requestId ?? null);
        return response.status(202).end();
      }
      throw error;
    }
    ```
  </Tab>
</Tabs>

See [UPG Errors](/universal-payment-gateway/errors) for error meanings and customer-safe handling guidance.

## Return a safe webhook acknowledgement

After the result is processed, return a short acknowledgement to the provider. Do not echo `completionContext`, raw provider data, `nativeOperations`, or `include` data.

<Tabs>
  <Tab title="PHP">
    ```php theme={null}
    http_response_code(200);
    header('Content-Type: application/json; charset=UTF-8');

    echo json_encode([
        'received' => true,
        'order_id' => $result->orderId,
    ]);
    ```
  </Tab>

  <Tab title="Laravel">
    ```php theme={null}
    http_response_code(200);
    header('Content-Type: application/json; charset=UTF-8');

    echo json_encode([
        'received' => true,
        'order_id' => $result->orderId,
    ]);
    ```
  </Tab>

  <Tab title="Node.js">
    ```ts theme={null}
    // Acknowledge only after your server has persisted the verified completion state.
    return response.status(204).end();
    ```
  </Tab>
</Tabs>

<Tip>
  Store your merchant order ID, the provider reference, and AvraAPI request ID together. They are the useful support and reconciliation correlation records; provider secrets and raw sensitive payloads are not.
</Tip>

Continue to [Redirect Checkout](/universal-payment-gateway/advanced-setup/redirect-checkout) or [In-page Checkout](/universal-payment-gateway/advanced-setup/hosted-checkout).


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