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

> Handle UPG readiness, typed SDK exceptions, and non-final payment results safely.

UPG has three different failure paths. Treating them as one creates unsafe retries and confusing buyer messages:

1. **Availability** is a successful, browser-safe preflight response. It tells you not to begin checkout.
2. **SDK exceptions** mean the request did not complete successfully. Handle them on your backend by exception type and request ID.
3. **Completion results** can be valid but non-final. A `pending`, `failed`, `cancelled`, or `unknown` result is not an exception and must not be treated as a successful order.

<Warning>
  Never return a signed completion context, gateway credentials, raw callback body, native provider response, or `exception->getPayload()` to the buyer. Store the request ID in protected backend logs and show the buyer only a safe next step.
</Warning>

## The API error envelope

For a non-2xx API response, the PHP SDK throws a typed exception. The source response uses this shape:

```json theme={null}
{
  "success": false,
  "request_id": "01j...",
  "error": {
    "code": "payment_mode_not_available",
    "message": "This checkout mode is not enabled for the selected gateway.",
    "details": {
      "mode": ["The selected mode is not available."]
    }
  }
}
```

`request_id`, `error.code`, and the HTTP status are stable backend diagnostic inputs. The human-readable message and optional `details` can change as validation improves, so do not build application logic around exact message text.

## Start with availability, not an error

Call `availability()` before rendering a new checkout. It returns `PaymentAvailability`, not an exception, when an active UPG slot or usable configuration is absent. Its payload is public and can safely be sent to Payment Elements.

| `reason` | Meaning | Safe application action |
| - | - | - |
| `upg_entitlement_inactive` | The project has no active UPG entitlement or slot. | Hide payment methods and ask the workspace owner to restore the service. |
| `upg_gateway_not_entitled` | Configurations exist, but the plan does not include an eligible gateway. | Do not show a method; review the workspace plan. |
| `payment_configuration_not_available` | No active, usable gateway profile is available for the project environment. | Show an unavailable checkout state; configure or reactivate a gateway profile. |
| `unknown` | A future or unrecognised public availability reason was returned. | Fail closed: do not start checkout and record the request ID. |

`project_paused` is also part of the public availability-reason enum, but a paused project is normally rejected by project authentication with HTTP `503` before UPG availability can be returned. Because the PHP SDK maps its UPG-specific error code before general HTTP status, it surfaces as `PaymentAccessException`.

<Tabs>
  <Tab title="PHP SDK">
    ```php theme={null}
    $availability = $apix->payment()->availability();

    if (! $availability->isReady()) {
        // Safe to use in a browser response: no credentials or Vault data exist here.
        return [
            'checkout_available' => false,
            'reason' => $availability->reason?->value,
            'message' => $availability->message,
        ];
    }

    // Use only methods where the server says available.
    $methods = array_filter(
        $availability->methods,
        static fn ($method): bool => $method->isAvailable(),
    );
    ```

    **SDK response** — unavailable availability result:

    ```json theme={null}
    {
      "ready": false,
      "reason": "upg_entitlement_inactive",
      "message": "Payment methods are currently unavailable for this project.",
      "methods": []
    }
    ```
  </Tab>

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

    $availability = AvraAPI::payment()->availability();

    if (! $availability->isReady()) {
        // Safe to use in a browser response: no credentials or Vault data exist here.
        return [
            'checkout_available' => false,
            'reason' => $availability->reason?->value,
            'message' => $availability->message,
        ];
    }

    // Use only methods where the server says available.
    $methods = array_filter(
        $availability->methods,
        static fn ($method): bool => $method->isAvailable(),
    );
    ```
  </Tab>

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

    if (!availability.isReady()) {
      return {
        checkout_available: false,
        reason: availability.reason,
        message: availability.message,
      };
    }

    const methods = availability.methods.filter((method) => method.available);
    ```
  </Tab>
</Tabs>

Refresh availability from your backend if the buyer leaves checkout open for a long time. A UPG slot or gateway configuration can change after a page first rendered. Do not use a cached method list to bypass the new-checkout authorization check.

## PHP SDK exception mapping

Every SDK-specific exception extends `ApixException`. You can catch that base class for logging, but payment flows should first handle the more specific type below. The PHP SDK maps by payment error-code prefix before its general HTTP-status mapping.

| SDK exception | Typical UPG code or condition | Safe backend action |
| - | - | - |
| `PaymentAccessException` | `upg_*`, `payment_configuration_not_available`, `project_paused` | Do not start a new checkout. Refresh availability or ask the workspace/project owner to restore access. |
| `PaymentConfigurationException` | `payment_gateway_not_available`, `payment_mode_not_available`, other `payment_configuration*` codes | Correct the selected gateway/mode or the Gateway Vault setup. Do not retry unchanged input. |
| `PaymentVerificationException` | `payment_callback_*` | Reject the callback as invalid or mismatched. Do not fulfil or retry altered evidence. |
| `PaymentProviderException` | Other `payment_*` codes, including provider, callback URL, and completion-processing failures | Follow the code-specific action; a transient provider failure may require bounded backend retry. |
| `ApixAuthenticationException` | HTTP `401` | Correct project credentials or `X-ENV` on the backend. Never ask the buyer for credentials. |
| `ApixValidationException` | Non-payment HTTP `422` validation error | Correct backend request input; inspect `getValidationErrors()` only in protected logs/tools. |
| `ApixRateLimitException` | HTTP `429` | Back off before a backend retry. Do not create a tight checkout retry loop. |
| `ApixServiceUnavailableException` | A non-UPG-specific HTTP `503` response | Keep the order unchanged and retry later only when appropriate. |
| `ApixNetworkException` | No HTTP response from AvraAPI | Treat the request outcome as unknown; do not blindly create a second checkout. |
| `ApixException` | Any other API error | Record the request ID and fail closed until the cause is understood. |

`payment_validation_error` begins with `payment_`, so the current PHP SDK maps it to `PaymentProviderException` even when the HTTP response is `422`. This is intentional: use `getErrorCode()` rather than assuming a type from the status code alone.

## Handle a new-checkout exception safely

Create a provider checkout session only on your backend. Do not automatically repeat a failed `createOrder()` call after a timeout or network error: the provider may have received the first request even if your application did not receive its response.

<Tabs>
  <Tab title="PHP SDK">
    ```php theme={null}
    use Avraapi\Apix\Exceptions\ApixException;
    use Avraapi\Apix\Exceptions\ApixNetworkException;
    use Avraapi\Apix\Exceptions\ApixRateLimitException;
    use Avraapi\Apix\Exceptions\ApixServiceUnavailableException;
    use Avraapi\Apix\Exceptions\PaymentAccessException;
    use Avraapi\Apix\Exceptions\PaymentConfigurationException;
    use Avraapi\Apix\Exceptions\PaymentProviderException;

    try {
        $session = $apix->payment()->createOrder($options);
    } catch (PaymentAccessException | PaymentConfigurationException $exception) {
        // Log $exception->getRequestId() on the backend.
        return ['checkout_available' => false, 'retry' => false];
    } catch (ApixRateLimitException | ApixServiceUnavailableException $exception) {
        return ['checkout_available' => false, 'retry' => true];
    } catch (ApixNetworkException $exception) {
        // The provider/session outcome is unknown. Reconcile the pending merchant order first.
        return ['checkout_available' => false, 'retry' => false];
    } catch (PaymentProviderException $exception) {
        return ['checkout_available' => false, 'retry' => false];
    } catch (ApixException $exception) {
        return ['checkout_available' => false, 'retry' => false];
    }
    ```

    **Your application response** — safe buyer-facing result:

    ```json theme={null}
    {
      "checkout_available": false,
      "retry": false
    }
    ```

    The example intentionally keeps the gateway error code and request ID on the backend. Your application can show a friendly unavailable or retry-later message without exposing configuration or provider diagnostics.
  </Tab>

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

    use Avraapi\Apix\Exceptions\ApixException;
    use Avraapi\Apix\Exceptions\ApixNetworkException;
    use Avraapi\Apix\Exceptions\ApixRateLimitException;
    use Avraapi\Apix\Exceptions\ApixServiceUnavailableException;
    use Avraapi\Apix\Exceptions\PaymentAccessException;
    use Avraapi\Apix\Exceptions\PaymentConfigurationException;
    use Avraapi\Apix\Exceptions\PaymentProviderException;

    try {
        $session = AvraAPI::payment()->createOrder($options);
    } catch (PaymentAccessException | PaymentConfigurationException $exception) {
        // Log $exception->getRequestId() on the backend.
        return ['checkout_available' => false, 'retry' => false];
    } catch (ApixRateLimitException | ApixServiceUnavailableException $exception) {
        return ['checkout_available' => false, 'retry' => true];
    } catch (ApixNetworkException $exception) {
        // The provider/session outcome is unknown. Reconcile the pending merchant order first.
        return ['checkout_available' => false, 'retry' => false];
    } catch (PaymentProviderException $exception) {
        return ['checkout_available' => false, 'retry' => false];
    } catch (ApixException $exception) {
        return ['checkout_available' => false, 'retry' => false];
    }
    ```
  </Tab>

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

    try {
      const session = await client.payment().createOrder(options);
    } catch (error) {
      if (error instanceof PaymentAccessError || error instanceof PaymentConfigurationError) {
        return { checkout_available: false, retry: false };
      }
      if (error instanceof ApixRateLimitError || error instanceof ApixServiceUnavailableError) {
        return { checkout_available: false, retry: true };
      }
      if (error instanceof ApixNetworkError || error instanceof PaymentProviderError || error instanceof ApixError) {
        // Preserve the pending order and reconcile before another checkout attempt.
        return { checkout_available: false, retry: false };
      }
      throw error;
    }
    ```
  </Tab>
</Tabs>

## Completion can be valid but non-final

Completion is different from new checkout. A valid completion context issued before a slot is released can still be verified so an existing payment is not abandoned. `completePayment()` may return a valid `PaymentCompletionResult` instead of throwing an exception.

| `paymentStatus` | Meaning | Order action |
| - | - | - |
| `succeeded` with `verified: true` | The gateway-specific completion rules were satisfied. | Fulfil exactly once. |
| `pending` | The provider has not reached a terminal state. | Keep the merchant order pending; use only documented, bounded backend reconciliation for that gateway. |
| `failed` | The payment failed according to the verified provider result. | Do not fulfil; offer a new checkout if appropriate. |
| `cancelled` | The verified provider result was cancelled or expired. | Do not fulfil; let the buyer start a new checkout if appropriate. |
| `unknown` | Provider evidence was unavailable, conflicting, or not safely final. | Do not fulfil; keep the order pending for controlled backend retry or support review. |

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

    $result = $apix->payment()->completePayment($completionOptions);

    if ($result->verified && $result->paymentStatus === PaymentStatus::Succeeded) {
        // Fulfil exactly once in your own order store.
    } elseif (in_array($result->paymentStatus, [PaymentStatus::Pending, PaymentStatus::Unknown], true)) {
        // Keep the order pending. Inspect only the safe reconciliation metadata.
        $retryRecommended = (bool) ($result->reconciliation['retry_recommended'] ?? false);
    } else {
        // Failed or cancelled: do not fulfil.
    }
    ```

    **SDK response** — non-final completion result:

    ```json theme={null}
    {
      "gateway": "stripe",
      "verified": false,
      "payment_status": "unknown",
      "provider_status": "UNKNOWN",
      "order_id": "ORDER-2026-000184",
      "gateway_reference": null,
      "amount": "12000.00",
      "currency": "LKR",
      "reconciliation": {
        "attempted": true,
        "state": "unavailable",
        "authority": "provider_status",
        "callback_status": "paid",
        "secondary_status": null,
        "retry_recommended": true
      }
    }
    ```

    `nativeOperations` and `include` are server-only response fields. The normal default is the short response; never forward `full()` or provider-native included fields to a browser.
  </Tab>

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

    use Avraapi\Apix\Payments\PaymentStatus;

    $result = AvraAPI::payment()->completePayment($completionOptions);

    if ($result->verified && $result->paymentStatus === PaymentStatus::Succeeded) {
        // Fulfil exactly once in your own order store.
    } elseif (in_array($result->paymentStatus, [PaymentStatus::Pending, PaymentStatus::Unknown], true)) {
        // Keep the order pending. Inspect only the safe reconciliation metadata.
        $retryRecommended = (bool) ($result->reconciliation['retry_recommended'] ?? false);
    } else {
        // Failed or cancelled: do not fulfil.
    }
    ```
  </Tab>

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

    const result = await client.payment().completePayment(completionOptions);

    if (result.verified && result.paymentStatus === PaymentStatus.Succeeded) {
      // Fulfil exactly once in your own order store.
    } else if ([PaymentStatus.Pending, PaymentStatus.Unknown].includes(result.paymentStatus)) {
      // Keep the order pending. Inspect only safe reconciliation metadata.
      const retryRecommended = result.reconciliation.retry_recommended === true;
    } else {
      // Failed or cancelled: do not fulfil.
    }
    ```
  </Tab>
</Tabs>

## Callback verification failures

If `completePayment()` throws `PaymentVerificationException`, do not retry the same modified callback or mark the order paid. It represents a `payment_callback_*` error such as a malformed callback, invalid callback signature, or callback/order mismatch.

Some gateways report signature failures under other `payment_*` codes and therefore surface as `PaymentProviderException`. The safe response is the same: keep the order unfulfilled, preserve the original evidence only in protected backend diagnostics, and investigate with the request ID.

## Retry rules

| Situation | Retry? | Safe rule |
| - | - | - |
| Availability says not ready | No automatic retry | Refresh on a later user action or after operations restores access. |
| Invalid gateway, mode, URLs, or configuration | No | Fix input or configuration first. |
| Callback verification failure | No | Reject it; never mutate or replay altered signed evidence. |
| `payment_completion_in_progress` | Yes, shortly | Another completion request holds the short-lived lock; retry the same stored context and original evidence from your backend. |
| Rate limit, provider temporary failure, or service unavailable | Controlled retry | Use bounded exponential backoff and preserve your merchant order correlation. |
| Network error during `createOrder()` | Do not blindly retry | The initial provider session may exist; reconcile your pending order before issuing another checkout. |
| `unknown` completion with `retry_recommended: true` | Controlled retry | Re-run completion from the backend with the original context and original verified evidence when supported. |

## What to log and what to show

Log only on the backend: request ID, stable error code, HTTP status, gateway code, your own pending-order ID, and safe `reconciliation` metadata. Keep callback signatures, completion contexts, raw bodies, provider-native fields, credentials, and customer/payment data out of logs unless a protected incident process explicitly requires them.

For buyers, use short messages such as “Payment methods are unavailable”, “Please try again later”, “Your payment is still being confirmed”, or “This payment was not completed.” Do not expose a gateway-specific failure reason or imply success until `verified` is true and `paymentStatus` is `succeeded`.


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